Feat add iam auth
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { Body, Controller, HttpCode, Logger, Post } from '@nestjs/common';
|
||||
import { Public } from '../auth/guards';
|
||||
import { AdminSessionService } from '../sessions/admin-session.service';
|
||||
import { AppSessionService } from '../sessions/app-session.service';
|
||||
import { IamLogoutService, IamLogoutTokenError } from './iam-logout.service';
|
||||
import { IamService } from './iam.service';
|
||||
|
||||
/**
|
||||
* Back-channel logout receiver (OpenID Connect Back-Channel Logout 1.0).
|
||||
*
|
||||
* Register this path as the client's `backchannel_logout_uri` in the IAM:
|
||||
*
|
||||
* https://<api-host>/api/iam/backchannel-logout
|
||||
*
|
||||
* The endpoint is public because it carries no credentials by design — its
|
||||
* authority is the logout token's signature, verified against the IAM's keys. It
|
||||
* always answers 200, as the spec asks, so a rejected token does not make the IAM
|
||||
* retry forever; the reason is logged here instead.
|
||||
*
|
||||
* Nothing in the UI changes: this is how a central "sign out everywhere" or an
|
||||
* administrator revoking a session actually ends a session in this app.
|
||||
*/
|
||||
@Controller('iam')
|
||||
export class IamController {
|
||||
private readonly logger = new Logger(IamController.name);
|
||||
|
||||
constructor(
|
||||
private readonly iam: IamService,
|
||||
private readonly logout: IamLogoutService,
|
||||
private readonly sessions: AdminSessionService,
|
||||
private readonly appSessions: AppSessionService,
|
||||
) {}
|
||||
|
||||
@Public()
|
||||
@Post('backchannel-logout')
|
||||
@HttpCode(200)
|
||||
async backchannelLogout(@Body() body: Record<string, unknown>) {
|
||||
if (!this.iam.enabled) {
|
||||
// Not configured: nothing to act on, and no information to give away.
|
||||
return { status: 'ignored' };
|
||||
}
|
||||
|
||||
const token = typeof body?.logout_token === 'string' ? body.logout_token : '';
|
||||
if (!token) {
|
||||
this.logger.warn('back-channel logout called without a logout token');
|
||||
return { status: 'ignored' };
|
||||
}
|
||||
|
||||
try {
|
||||
const { sessionId, subject } = await this.logout.verify(token);
|
||||
|
||||
// `sid` names one session; a token with only `sub` means every session of
|
||||
// that identity — matching the spec's two forms.
|
||||
//
|
||||
// Both token families are ended: the admin panel AND the app (customer /
|
||||
// sales expert). Leaving the app out would make a central sign-out look
|
||||
// like it worked while the phone in the user's pocket stayed signed in.
|
||||
const [admin, app] = sessionId
|
||||
? await Promise.all([
|
||||
this.sessions.revokeByIamSession(sessionId),
|
||||
this.appSessions.revokeByIamSession(sessionId),
|
||||
])
|
||||
: await Promise.all([
|
||||
this.sessions.revokeByIamSubject(subject as string),
|
||||
this.appSessions.revokeByIamSubject(subject as string),
|
||||
]);
|
||||
|
||||
const revoked = admin + app;
|
||||
this.logger.log(
|
||||
`back-channel logout accepted (${sessionId ? `sid=${sessionId}` : `sub=${subject}`}): ` +
|
||||
`${admin} admin + ${app} app session(s) revoked`,
|
||||
);
|
||||
|
||||
return { status: 'ok', revoked };
|
||||
} catch (error) {
|
||||
// A bad token is a security event worth logging, not a 4xx worth retrying.
|
||||
const detail = (error as Error).message;
|
||||
if (error instanceof IamLogoutTokenError) {
|
||||
this.logger.warn(`rejected a back-channel logout token: ${detail}`);
|
||||
} else {
|
||||
this.logger.error(`back-channel logout could not be processed: ${detail}`);
|
||||
}
|
||||
|
||||
return { status: 'ignored' };
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user