import { Injectable } from '@nestjs/common';
import type { Request } from 'express';
import { createHash } from 'node:crypto';
import { createRemoteJWKSet,jwtVerify,type JWTPayload } from 'jose';
import { DatabaseService } from '../../infrastructure/database/database.service';
import type { AuthProvider } from './auth-provider';
import type { AuthenticatedPrincipal } from './authenticated-principal';
@Injectable()
export class OidcAuthProvider implements AuthProvider {
private readonly issuer=process.env.OIDC_ISSUER;
private readonly audience=process.env.OIDC_AUDIENCE;
private readonly jwksUri=process.env.OIDC_JWKS_URI;
private readonly jwks=this.jwksUri
? createRemoteJWKSet(new URL(this.jwksUri),{
cooldownDuration:30_000,
cacheMaxAge:10*60_000,
timeoutDuration:5_000,
})
: null;
constructor(private readonly database:DatabaseService) {}
async authenticate(request:Request):Promise<AuthenticatedPrincipal|null> {
if(process.env.AUTH_ADAPTER!=='oidc') return null;
if(!this.issuer || !this.audience || !this.jwks) throw new Error('auth.oidc_not_configured');
const header=request.header('authorization')??'';
const match=/^Bearer\s+(.+)$/i.exec(header);
if(!match) return null;
let payload:JWTPayload;
try {
const verified=await jwtVerify(match[1],this.jwks,{
issuer:this.issuer,
audience:this.audience,
clockTolerance:Number(process.env.OIDC_CLOCK_TOLERANCE_SECONDS??30),
});
payload=verified.payload;
} catch {
return null;
}
const subject=String(payload.sub??'');
if(!subject || !payload.iat) return null;
const users=await this.database.query<any>(
`select ua.id,ua.external_auth_subject,ua.preferred_locale,ua.status,
uas.revoked_before,uas.require_step_up_after
from user_accounts ua
left join user_auth_security_state uas on uas.user_account_id=ua.id
where ua.external_auth_subject=$1 and ua.status='active'
limit 1`,
[subject],
);
const user=users[0];
if(!user) return null;
const issuedAt=Number(payload.iat);
if(user.revoked_before && issuedAt*1000<=new Date(user.revoked_before).getTime()) return null;
const sessionId=payload.sid?String(payload.sid):undefined;
const tokenId=payload.jti?String(payload.jti):undefined;
const identifier=sessionId??tokenId;
if(identifier) {
const hash=createHash('sha256')
.update(`${process.env.SECURITY_HASH_SALT??'dev-only'}:${identifier}`)
.digest('hex');
const revoked=await this.database.query<any>(
`select 1 from revoked_auth_sessions
where session_identifier_hash=$1
and (expires_at is null or expires_at>now())
limit 1`,
[hash],
);
if(revoked[0]) return null;
}
const rawAmr=(payload as any).amr;
const amr=Array.isArray(rawAmr)?rawAmr.map(String):rawAmr?[String(rawAmr)]:[];
return {
userId:user.id,
authSubject:subject,
locale:user.preferred_locale||'en-GB',
issuedAt,
authTime:(payload as any).auth_time?Number((payload as any).auth_time):undefined,
sessionId,
tokenId,
acr:(payload as any).acr?String((payload as any).acr):undefined,
amr,
};
}
}
