feat: revocable read-only API keys for machine access
Integrations such as the Wabenhain shop sync no longer need a service account with a password (and without MFA). An admin creates a key under Verwaltung > Automatisierung > API-Schlüssel. - Key format ack_<8>_<43>; only its SHA-256 hash is stored, the key is shown exactly once. Optional expiry, "last used" timestamp, revocation takes effect immediately; create/revoke are audit-logged without the key. - A key is not a user: JwtAuthGuard sets request.apiKey, PermissionsGuard admits it only to routes whose permission is among its scopes and never to user-only routes. Scopes come from a short allowlist (shop-sync:read). - Managing keys requires the new permission api-keys:manage. - Keys are excluded from backups on purpose; a restore means issuing a new key. Additive migration (new table). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
ede86c062a
commit
445fb3b94b
21 files changed
+848
-5
No files matched your search
@@ -8,9 +8,16 @@ import { AuthGuard } from '@nestjs/passport';
|
||||
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
||||
import { PermissionResolverService } from '../permissions/permission-resolver.service';
|
||||
import { AuthenticatedUser } from '../types/authenticated-user';
|
||||
import {
|
||||
ApiKeyPrincipal,
|
||||
ApiKeysService,
|
||||
extractApiKeyToken,
|
||||
} from '../../api-keys/api-keys.service';
|
||||
|
||||
interface RequestWithUser {
|
||||
headers: { authorization?: string };
|
||||
user?: AuthenticatedUser;
|
||||
apiKey?: ApiKeyPrincipal;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,6 +35,7 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
private readonly permissionResolver: PermissionResolverService,
|
||||
private readonly apiKeys: ApiKeysService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -40,11 +48,25 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
const request = context.switchToHttp().getRequest<RequestWithUser>();
|
||||
|
||||
// An API key ("Bearer ack_…") authenticates a machine, not a user: no
|
||||
// request.user is set, so it can never pass a user-only route, and
|
||||
// PermissionsGuard checks its scopes instead of a role.
|
||||
const apiKeyToken = extractApiKeyToken(request.headers.authorization);
|
||||
if (apiKeyToken !== null) {
|
||||
const principal = await this.apiKeys.authenticate(apiKeyToken);
|
||||
if (!principal) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
request.apiKey = principal;
|
||||
return true;
|
||||
}
|
||||
|
||||
const authenticated = (await super.canActivate(context)) as boolean;
|
||||
if (!authenticated) {
|
||||
return false;
|
||||
}
|
||||
const request = context.switchToHttp().getRequest<RequestWithUser>();
|
||||
const user = request.user;
|
||||
if (
|
||||
!user ||
|
||||
|
||||
Reference in new issue
Block a user