mirror of https://github.com/ghostfolio/ghostfolio
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
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;
|
|
}
|
|
}
|
|
}
|
|
|