Files
ApiCore/ApiCore-Backend/src/auth/guards/jwt-auth.guard.ts
T
Nick SzaunigandClaude Opus 5.5 f1dc0de3e2 feat(api-keys): restrict keys to source IP addresses
A key can be limited to IP addresses or CIDR networks (e.g. the Wabenhain
server); a stolen key is useless from anywhere else. Requests from other
addresses get 403 "not allowed for this IP" and a log line. The admin UI
asks for the addresses when creating a key, warns when a key works from
anywhere, shows the last source address and can take it over into the
allowlist - changing the list needs no new key (PATCH /api-keys/:id).

Also: a key can only carry scopes its creator holds.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-01 02:41:46 +02:00

85 lines
2.7 KiB
TypeScript

import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
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 };
ip?: string;
user?: AuthenticatedUser;
apiKey?: ApiKeyPrincipal;
}
/**
* Enforces User.isActive and token freshness (issued after the last
* password change) on every authenticated request, not just ones gated by
* @RequirePermission. Without this, a deactivated user's still-valid access
* token - or one stolen before a password change - would keep working on
* any route that carries no permission decorator until the token's own
* expiry (up to JWT_ACCESS_EXPIRES_IN). isTokenValid shares
* PermissionsGuard's cache, so this check costs no extra query beyond what
* permission resolution already does.
*/
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
private readonly reflector: Reflector,
private readonly permissionResolver: PermissionResolverService,
private readonly apiKeys: ApiKeysService,
) {
super();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
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) {
// request.ip honours TRUST_PROXY (see app.config.ts).
const principal = await this.apiKeys.authenticate(
apiKeyToken,
request.ip,
);
if (!principal) {
throw new UnauthorizedException();
}
request.apiKey = principal;
return true;
}
const authenticated = (await super.canActivate(context)) as boolean;
if (!authenticated) {
return false;
}
const user = request.user;
if (
!user ||
!(await this.permissionResolver.isTokenValid(user.id, user.tokenIssuedAt))
) {
throw new UnauthorizedException();
}
return true;
}
}