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.
120 lines
3.5 KiB
120 lines
3.5 KiB
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
|
|
|
import { Injectable, Logger } from '@nestjs/common';
|
|
import { PassportStrategy } from '@nestjs/passport';
|
|
import { Provider } from '@prisma/client';
|
|
import { Strategy } from 'passport-openidconnect';
|
|
|
|
import { AuthService } from './auth.service';
|
|
import { OidcStateStore } from './oidc-state.store';
|
|
|
|
interface OidcDiscovery {
|
|
authorization_endpoint: string;
|
|
issuer: string;
|
|
token_endpoint: string;
|
|
userinfo_endpoint: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
|
|
private static readonly logger = new Logger(OidcStrategy.name);
|
|
private static readonly stateStore = new OidcStateStore();
|
|
|
|
public constructor(
|
|
private readonly authService: AuthService,
|
|
configurationService: ConfigurationService
|
|
) {
|
|
const issuer = configurationService.get('OIDC_ISSUER');
|
|
const clientID = configurationService.get('OIDC_CLIENT_ID');
|
|
const clientSecret = configurationService.get('OIDC_CLIENT_SECRET');
|
|
const callbackURL =
|
|
configurationService.get('OIDC_CALLBACK_URL') ||
|
|
`${configurationService.get('ROOT_URL')}/api/auth/oidc/callback`;
|
|
|
|
const scope =
|
|
configurationService.get('OIDC_SCOPE') || 'openid profile email';
|
|
|
|
// Use explicit URLs if provided
|
|
const authorizationURL = configurationService.get('OIDC_AUTHORIZATION_URL');
|
|
const tokenURL = configurationService.get('OIDC_TOKEN_URL');
|
|
const userInfoURL = configurationService.get('OIDC_USER_INFO_URL');
|
|
|
|
const strategyConfig: any = {
|
|
authorizationURL: authorizationURL || `${issuer}authorize/`,
|
|
callbackURL,
|
|
clientID,
|
|
clientSecret,
|
|
issuer,
|
|
passReqToCallback: true,
|
|
scope: scope.split(' '),
|
|
store: OidcStrategy.stateStore,
|
|
tokenURL: tokenURL || `${issuer}token/`,
|
|
userInfoURL: userInfoURL || `${issuer}userinfo/`
|
|
};
|
|
|
|
OidcStrategy.logger.log(
|
|
`OIDC authentication configured with issuer: ${issuer}`
|
|
);
|
|
|
|
super(strategyConfig);
|
|
}
|
|
|
|
/**
|
|
* Static method to fetch OIDC discovery configuration
|
|
*/
|
|
public static async fetchDiscoveryConfig(
|
|
issuer: string
|
|
): Promise<OidcDiscovery> {
|
|
const discoveryUrl = `${issuer}.well-known/openid-configuration`;
|
|
|
|
OidcStrategy.logger.log(
|
|
`Fetching OIDC configuration from: ${discoveryUrl}`
|
|
);
|
|
|
|
const response = await fetch(discoveryUrl);
|
|
const discovery = (await response.json()) as OidcDiscovery;
|
|
|
|
OidcStrategy.logger.log(
|
|
`OIDC discovery successful. Authorization endpoint: ${discovery.authorization_endpoint}`
|
|
);
|
|
|
|
return discovery;
|
|
}
|
|
|
|
public async validate(
|
|
_request: any,
|
|
_issuer: string,
|
|
profile: any,
|
|
context: any,
|
|
idToken: any,
|
|
_accessToken: any,
|
|
_refreshToken: any,
|
|
params: any
|
|
) {
|
|
const thirdPartyId =
|
|
params?.sub || idToken?.sub || context?.claims?.sub || profile?.id;
|
|
|
|
if (!thirdPartyId) {
|
|
OidcStrategy.logger.error(
|
|
'No subject (sub) found in OIDC token or profile',
|
|
{ context, idToken, params, profile }
|
|
);
|
|
throw new Error('Missing subject identifier in OIDC response');
|
|
}
|
|
|
|
OidcStrategy.logger.debug(
|
|
`Validating OIDC user with subject: ${thirdPartyId}`
|
|
);
|
|
|
|
const jwt = await this.authService.validateOAuthLogin({
|
|
provider: Provider.OIDC,
|
|
thirdPartyId
|
|
});
|
|
|
|
OidcStrategy.logger.log(
|
|
`Successfully authenticated OIDC user: ${thirdPartyId}`
|
|
);
|
|
|
|
return { jwt };
|
|
}
|
|
}
|
|
|