You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

105 lines
2.8 KiB

import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Provider } from '@prisma/client';
import { Request } from 'express';
import { Strategy, type StrategyOptions } from 'passport-openidconnect';
import { AuthService } from './auth.service';
import {
OidcContext,
OidcIdToken,
OidcParams,
OidcProfile,
OidcValidationResult
} from './interfaces/interfaces';
import { OidcStateStore } from './oidc-state.store';
@Injectable()
export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
public constructor(
private readonly authService: AuthService,
private readonly jwtService: JwtService,
stateStore: OidcStateStore,
options: StrategyOptions
) {
super({
...options,
passReqToCallback: true,
store: stateStore
});
}
public async validate(
request: Request,
issuer: string,
profile: OidcProfile,
context: OidcContext,
idToken: OidcIdToken,
_accessToken: string,
_refreshToken: string,
params: OidcParams
) {
try {
const thirdPartyId =
profile?.id ??
profile?.sub ??
idToken?.sub ??
params?.sub ??
context?.claims?.sub;
if (!thirdPartyId) {
Logger.error(
`Missing subject identifier in OIDC response from ${issuer}`,
'OidcStrategy'
);
throw new Error('Missing subject identifier in OIDC response');
}
// Check if user is already authenticated via JWT
// If authenticated, this is a link operation; otherwise, normal login
// The linkToken is attached by OidcStateStore.verify() from the OAuth state
const linkToken = (request as any).oidcLinkToken as string | undefined;
const authenticatedUserId = this.extractAuthenticatedUserId(linkToken);
if (authenticatedUserId) {
// User is authenticated → Link mode
// Return linkState for controller to handle linking
return {
linkState: {
userId: authenticatedUserId
},
thirdPartyId
} as OidcValidationResult;
}
// No authenticated user → Normal OIDC login flow
const jwt = await this.authService.validateOAuthLogin({
thirdPartyId,
provider: Provider.OIDC
});
return { jwt, thirdPartyId } as OidcValidationResult;
} catch (error) {
Logger.error(error, 'OidcStrategy');
throw error;
}
}
/**
* Extract authenticated user ID from linkToken passed via OAuth state
*/
private extractAuthenticatedUserId(linkToken?: string): string | null {
if (!linkToken) {
return null;
}
try {
const decoded = this.jwtService.verify<{ id: string }>(linkToken);
return decoded?.id || null;
} catch {
return null;
}
}
}