/** * TASK-0132 — SCIM bearer-token guard (token → org). * * `Authorization: Bearer scim_…` on `/scim/v2/*`. Resolves the raw token to * an org via the SHA-256 hash index in Redis (no DB scan per request); * loads the full org row (the services need `scimConfig`). A missing or * unknown token → `401 { scimType: "invalidCredentials" }`. The org is * stashed on `req.scimOrg` for the controller (the token acts as the org — * audited as actorType INTEGRATION, actorId = orgId). */ import { CanActivate, ExecutionContext, Inject, Injectable } from "@nestjs/common"; import type { Request } from "express"; import { ScimAuthService } from "./scim-auth.service.js"; import { ScimError } from "./scim-errors.js"; import { PrismaService } from "../../common/prisma.service.js"; @Injectable() export class ScimBearerGuard implements CanActivate { constructor( @Inject(ScimAuthService) private readonly auth: ScimAuthService, @Inject(PrismaService) private readonly prisma: PrismaService ) {} async canActivate(context: ExecutionContext): Promise { const req = context.switchToHttp().getRequest(); const header = req.headers.authorization; if (!header || !header.startsWith("Bearer ")) { throw ScimError.unauthorized("Missing bearer token."); } const token = header.slice(7).trim(); if (!token) throw ScimError.unauthorized("Missing bearer token."); const resolved = await this.auth.resolveOrg(token); if (!resolved) throw ScimError.unauthorized(); // The token is per-org — load the org (the services need scimConfig). const org = await this.prisma.organization.findUnique({ where: { id: resolved.orgId } }); if (!org || org.scimConfig == null) { // Config removed mid-flight (disable) — invalidate immediately. throw ScimError.unauthorized(); } req.scimOrg = { id: org.id, scimConfig: org.scimConfig }; return true; } }