diff --git a/CHANGELOG.md b/CHANGELOG.md index 878d90326..7b3f93910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -#### Added +### Added - Introduced data source transformation support in the import functionality for self-hosted environments +- Added _OpenID Connect_ (`OIDC`) as a new login provider for self-hosted environments (experimental) - Added an optional 3D hover effect to the membership card component -#### Changed +### Changed - Increased the numerical precision for cryptocurrency quantities in the holding detail dialog - Upgraded `envalid` from version `8.1.0` to `8.1.1` diff --git a/apps/api/src/app/auth/auth.controller.ts b/apps/api/src/app/auth/auth.controller.ts index b45e7b97b..388f1dbd3 100644 --- a/apps/api/src/app/auth/auth.controller.ts +++ b/apps/api/src/app/auth/auth.controller.ts @@ -84,7 +84,6 @@ export class AuthController { @Req() request: Request, @Res() response: Response ) { - // Handles the Google OAuth2 callback const jwt: string = (request.user as any).jwt; if (jwt) { @@ -102,6 +101,46 @@ export class AuthController { } } + @Get('oidc') + @UseGuards(AuthGuard('oidc')) + @Version(VERSION_NEUTRAL) + public oidcLogin() { + if (!this.configurationService.get('ENABLE_FEATURE_AUTH_OIDC')) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + } + + @Get('oidc/callback') + @UseGuards(AuthGuard('oidc')) + @Version(VERSION_NEUTRAL) + public oidcLoginCallback(@Req() request: Request, @Res() response: Response) { + const jwt: string = (request.user as any).jwt; + + if (jwt) { + response.redirect( + `${this.configurationService.get( + 'ROOT_URL' + )}/${DEFAULT_LANGUAGE_CODE}/auth/${jwt}` + ); + } else { + response.redirect( + `${this.configurationService.get( + 'ROOT_URL' + )}/${DEFAULT_LANGUAGE_CODE}/auth` + ); + } + } + + @Post('webauthn/generate-authentication-options') + public async generateAuthenticationOptions( + @Body() body: { deviceId: string } + ) { + return this.webAuthService.generateAuthenticationOptions(body.deviceId); + } + @Get('webauthn/generate-registration-options') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async generateRegistrationOptions() { @@ -116,13 +155,6 @@ export class AuthController { return this.webAuthService.verifyAttestation(body.credential); } - @Post('webauthn/generate-authentication-options') - public async generateAuthenticationOptions( - @Body() body: { deviceId: string } - ) { - return this.webAuthService.generateAuthenticationOptions(body.deviceId); - } - @Post('webauthn/verify-authentication') public async verifyAuthentication( @Body() body: { deviceId: string; credential: AssertionCredentialJSON } diff --git a/apps/api/src/app/auth/auth.module.ts b/apps/api/src/app/auth/auth.module.ts index 824c432b1..9fc5d0925 100644 --- a/apps/api/src/app/auth/auth.module.ts +++ b/apps/api/src/app/auth/auth.module.ts @@ -4,17 +4,20 @@ import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscriptio import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { ApiKeyService } from '@ghostfolio/api/services/api-key/api-key.service'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; -import { Module } from '@nestjs/common'; +import { Logger, Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; +import type { StrategyOptions } from 'passport-openidconnect'; import { ApiKeyStrategy } from './api-key.strategy'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { GoogleStrategy } from './google.strategy'; import { JwtStrategy } from './jwt.strategy'; +import { OidcStrategy } from './oidc.strategy'; @Module({ controllers: [AuthController], @@ -36,6 +39,83 @@ import { JwtStrategy } from './jwt.strategy'; AuthService, GoogleStrategy, JwtStrategy, + { + inject: [AuthService, ConfigurationService], + provide: OidcStrategy, + useFactory: async ( + authService: AuthService, + configurationService: ConfigurationService + ) => { + const isOidcEnabled = configurationService.get( + 'ENABLE_FEATURE_AUTH_OIDC' + ); + + if (!isOidcEnabled) { + return null; + } + + const issuer = configurationService.get('OIDC_ISSUER'); + const scope = configurationService.get('OIDC_SCOPE'); + + const callbackUrl = + configurationService.get('OIDC_CALLBACK_URL') || + `${configurationService.get('ROOT_URL')}/api/auth/oidc/callback`; + + // Check for manual URL overrides + const manualAuthorizationUrl = configurationService.get( + 'OIDC_AUTHORIZATION_URL' + ); + const manualTokenUrl = configurationService.get('OIDC_TOKEN_URL'); + const manualUserInfoUrl = + configurationService.get('OIDC_USER_INFO_URL'); + + let authorizationURL: string; + let tokenURL: string; + let userInfoURL: string; + + if (manualAuthorizationUrl && manualTokenUrl && manualUserInfoUrl) { + // Use manual URLs + authorizationURL = manualAuthorizationUrl; + tokenURL = manualTokenUrl; + userInfoURL = manualUserInfoUrl; + } else { + // Fetch OIDC configuration from discovery endpoint + try { + const response = await fetch( + `${issuer}/.well-known/openid-configuration` + ); + + const config = (await response.json()) as { + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + }; + + // Manual URLs take priority over discovered ones + authorizationURL = + manualAuthorizationUrl || config.authorization_endpoint; + tokenURL = manualTokenUrl || config.token_endpoint; + userInfoURL = manualUserInfoUrl || config.userinfo_endpoint; + } catch (error) { + Logger.error(error, 'OidcStrategy'); + throw new Error('Failed to fetch OIDC configuration from issuer'); + } + } + + const options: StrategyOptions = { + authorizationURL, + issuer, + scope, + tokenURL, + userInfoURL, + callbackURL: callbackUrl, + clientID: configurationService.get('OIDC_CLIENT_ID'), + clientSecret: configurationService.get('OIDC_CLIENT_SECRET') + }; + + return new OidcStrategy(authService, options); + } + }, WebAuthService ] }) diff --git a/apps/api/src/app/auth/auth.service.ts b/apps/api/src/app/auth/auth.service.ts index a6ee5d260..6fe50dce0 100644 --- a/apps/api/src/app/auth/auth.service.ts +++ b/apps/api/src/app/auth/auth.service.ts @@ -17,30 +17,22 @@ export class AuthService { ) {} public async validateAnonymousLogin(accessToken: string): Promise { - return new Promise(async (resolve, reject) => { - try { - const hashedAccessToken = this.userService.createAccessToken({ - password: accessToken, - salt: this.configurationService.get('ACCESS_TOKEN_SALT') - }); + const hashedAccessToken = this.userService.createAccessToken({ + password: accessToken, + salt: this.configurationService.get('ACCESS_TOKEN_SALT') + }); - const [user] = await this.userService.users({ - where: { accessToken: hashedAccessToken } - }); + const [user] = await this.userService.users({ + where: { accessToken: hashedAccessToken } + }); - if (user) { - const jwt = this.jwtService.sign({ - id: user.id - }); + if (user) { + return this.jwtService.sign({ + id: user.id + }); + } - resolve(jwt); - } else { - throw new Error(); - } - } catch { - reject(); - } - }); + throw new Error(); } public async validateOAuthLogin({ @@ -75,7 +67,7 @@ export class AuthService { } catch (error) { throw new InternalServerErrorException( 'validateOAuthLogin', - error.message + error instanceof Error ? error.message : 'Unknown error' ); } } diff --git a/apps/api/src/app/auth/interfaces/interfaces.ts b/apps/api/src/app/auth/interfaces/interfaces.ts index 4fdcc25b5..7ddfe41d2 100644 --- a/apps/api/src/app/auth/interfaces/interfaces.ts +++ b/apps/api/src/app/auth/interfaces/interfaces.ts @@ -6,6 +6,25 @@ export interface AuthDeviceDialogParams { authDevice: AuthDeviceDto; } +export interface OidcContext { + claims?: { + sub?: string; + }; +} + +export interface OidcIdToken { + sub?: string; +} + +export interface OidcParams { + sub?: string; +} + +export interface OidcProfile { + id?: string; + sub?: string; +} + export interface ValidateOAuthLoginParams { provider: Provider; thirdPartyId: string; diff --git a/apps/api/src/app/auth/oidc-state.store.ts b/apps/api/src/app/auth/oidc-state.store.ts new file mode 100644 index 000000000..653451166 --- /dev/null +++ b/apps/api/src/app/auth/oidc-state.store.ts @@ -0,0 +1,114 @@ +import ms from 'ms'; + +/** + * Custom state store for OIDC authentication that doesn't rely on express-session. + * This store manages OAuth2 state parameters in memory with automatic cleanup. + */ +export class OidcStateStore { + private readonly STATE_EXPIRY_MS = ms('10 minutes'); + + private stateMap = new Map< + string, + { + appState?: unknown; + ctx: { issued?: Date; maxAge?: number; nonce?: string }; + meta?: unknown; + timestamp: number; + } + >(); + + /** + * Store request state. + * Signature matches passport-openidconnect SessionStore + */ + public store( + _req: unknown, + _meta: unknown, + appState: unknown, + ctx: { maxAge?: number; nonce?: string; issued?: Date }, + callback: (err: Error | null, handle?: string) => void + ) { + try { + // Generate a unique handle for this state + const handle = this.generateHandle(); + + this.stateMap.set(handle, { + appState, + ctx, + meta: _meta, + timestamp: Date.now() + }); + + // Clean up expired states + this.cleanup(); + + callback(null, handle); + } catch (error) { + callback(error as Error); + } + } + + /** + * Verify request state. + * Signature matches passport-openidconnect SessionStore + */ + public verify( + _req: unknown, + handle: string, + callback: ( + err: Error | null, + appState?: unknown, + ctx?: { maxAge?: number; nonce?: string; issued?: Date } + ) => void + ) { + try { + const data = this.stateMap.get(handle); + + if (!data) { + return callback(null, undefined, undefined); + } + + if (Date.now() - data.timestamp > this.STATE_EXPIRY_MS) { + // State has expired + this.stateMap.delete(handle); + return callback(null, undefined, undefined); + } + + // Remove state after verification (one-time use) + this.stateMap.delete(handle); + + callback(null, data.ctx, data.appState); + } catch (error) { + callback(error as Error); + } + } + + /** + * Clean up expired states + */ + private cleanup() { + const now = Date.now(); + const expiredKeys: string[] = []; + + for (const [key, value] of this.stateMap.entries()) { + if (now - value.timestamp > this.STATE_EXPIRY_MS) { + expiredKeys.push(key); + } + } + + for (const key of expiredKeys) { + this.stateMap.delete(key); + } + } + + /** + * Generate a cryptographically secure random handle + */ + private generateHandle() { + return ( + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15) + + Date.now().toString(36) + ); + } +} diff --git a/apps/api/src/app/auth/oidc.strategy.ts b/apps/api/src/app/auth/oidc.strategy.ts new file mode 100644 index 000000000..96b284121 --- /dev/null +++ b/apps/api/src/app/auth/oidc.strategy.ts @@ -0,0 +1,69 @@ +import { Injectable, Logger } from '@nestjs/common'; +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 +} from './interfaces/interfaces'; +import { OidcStateStore } from './oidc-state.store'; + +@Injectable() +export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') { + private static readonly stateStore = new OidcStateStore(); + + public constructor( + private readonly authService: AuthService, + options: StrategyOptions + ) { + super({ + ...options, + passReqToCallback: true, + store: OidcStrategy.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; + + const jwt = await this.authService.validateOAuthLogin({ + thirdPartyId, + provider: Provider.OIDC + }); + + if (!thirdPartyId) { + Logger.error( + `Missing subject identifier in OIDC response from ${issuer}`, + 'OidcStrategy' + ); + + throw new Error('Missing subject identifier in OIDC response'); + } + + return { jwt }; + } catch (error) { + Logger.error(error, 'OidcStrategy'); + throw error; + } + } +} diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 634fc959c..3802e3ef4 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -55,6 +55,10 @@ export class InfoService { globalPermissions.push(permissions.enableAuthGoogle); } + if (this.configurationService.get('ENABLE_FEATURE_AUTH_OIDC')) { + globalPermissions.push(permissions.enableAuthOidc); + } + if (this.configurationService.get('ENABLE_FEATURE_AUTH_TOKEN')) { globalPermissions.push(permissions.enableAuthToken); } diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index f37189569..a91aa6e69 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -41,6 +41,7 @@ export class ConfigurationService { default: [] }), ENABLE_FEATURE_AUTH_GOOGLE: bool({ default: false }), + ENABLE_FEATURE_AUTH_OIDC: bool({ default: false }), ENABLE_FEATURE_AUTH_TOKEN: bool({ default: true }), ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }), ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES: bool({ default: true }), @@ -54,9 +55,32 @@ export class ConfigurationService { GOOGLE_SHEETS_ID: str({ default: '' }), GOOGLE_SHEETS_PRIVATE_KEY: str({ default: '' }), HOST: host({ default: DEFAULT_HOST }), - JWT_SECRET_KEY: str({}), + JWT_SECRET_KEY: str(), MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }), MAX_CHART_ITEMS: num({ default: 365 }), + OIDC_AUTHORIZATION_URL: str({ default: '' }), + OIDC_CALLBACK_URL: str({ default: '' }), + OIDC_CLIENT_ID: str({ + default: undefined, + requiredWhen: (env) => { + return env.ENABLE_FEATURE_AUTH_OIDC === true; + } + }), + OIDC_CLIENT_SECRET: str({ + default: undefined, + requiredWhen: (env) => { + return env.ENABLE_FEATURE_AUTH_OIDC === true; + } + }), + OIDC_ISSUER: str({ + default: undefined, + requiredWhen: (env) => { + return env.ENABLE_FEATURE_AUTH_OIDC === true; + } + }), + OIDC_SCOPE: json({ default: ['openid'] }), + OIDC_TOKEN_URL: str({ default: '' }), + OIDC_USER_INFO_URL: str({ default: '' }), PORT: port({ default: DEFAULT_PORT }), PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: num({ default: DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 3a2ac687c..3c03744f1 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -17,6 +17,7 @@ export interface Environment extends CleanedEnvAccessors { DATA_SOURCES: string[]; DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER: string[]; ENABLE_FEATURE_AUTH_GOOGLE: boolean; + ENABLE_FEATURE_AUTH_OIDC: boolean; ENABLE_FEATURE_AUTH_TOKEN: boolean; ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean; ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES: boolean; @@ -32,6 +33,14 @@ export interface Environment extends CleanedEnvAccessors { JWT_SECRET_KEY: string; MAX_ACTIVITIES_TO_IMPORT: number; MAX_CHART_ITEMS: number; + OIDC_AUTHORIZATION_URL: string; + OIDC_CALLBACK_URL: string; + OIDC_CLIENT_ID: string; + OIDC_CLIENT_SECRET: string; + OIDC_ISSUER: string; + OIDC_SCOPE: string[]; + OIDC_TOKEN_URL: string; + OIDC_USER_INFO_URL: string; PORT: number; PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: number; PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY: number; diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts index 9fb9a8351..80239d56f 100644 --- a/apps/client/src/app/components/header/header.component.ts +++ b/apps/client/src/app/components/header/header.component.ts @@ -105,6 +105,7 @@ export class GfHeaderComponent implements OnChanges { public hasFilters: boolean; public hasImpersonationId: boolean; public hasPermissionForAuthGoogle: boolean; + public hasPermissionForAuthOidc: boolean; public hasPermissionForAuthToken: boolean; public hasPermissionForSubscription: boolean; public hasPermissionToAccessAdminControl: boolean; @@ -170,6 +171,11 @@ export class GfHeaderComponent implements OnChanges { permissions.enableAuthGoogle ); + this.hasPermissionForAuthOidc = hasPermission( + this.info?.globalPermissions, + permissions.enableAuthOidc + ); + this.hasPermissionForAuthToken = hasPermission( this.info?.globalPermissions, permissions.enableAuthToken @@ -286,6 +292,7 @@ export class GfHeaderComponent implements OnChanges { data: { accessToken: '', hasPermissionToUseAuthGoogle: this.hasPermissionForAuthGoogle, + hasPermissionToUseAuthOidc: this.hasPermissionForAuthOidc, hasPermissionToUseAuthToken: this.hasPermissionForAuthToken, title: $localize`Sign in` }, diff --git a/apps/client/src/app/components/login-with-access-token-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/login-with-access-token-dialog/interfaces/interfaces.ts index c7c4ab3fd..e9222e142 100644 --- a/apps/client/src/app/components/login-with-access-token-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/login-with-access-token-dialog/interfaces/interfaces.ts @@ -1,6 +1,7 @@ export interface LoginWithAccessTokenDialogParams { accessToken: string; hasPermissionToUseAuthGoogle: boolean; + hasPermissionToUseAuthOidc: boolean; hasPermissionToUseAuthToken: boolean; title: string; } diff --git a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html index bc232cfb7..cf5611ef7 100644 --- a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html +++ b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -45,6 +45,17 @@ > } + + @if (data.hasPermissionToUseAuthOidc) { +
+ Sign in with OIDC +
+ } diff --git a/libs/common/src/lib/permissions.ts b/libs/common/src/lib/permissions.ts index 597c27690..2e244568c 100644 --- a/libs/common/src/lib/permissions.ts +++ b/libs/common/src/lib/permissions.ts @@ -29,6 +29,7 @@ export const permissions = { deleteUser: 'deleteUser', deleteWatchlistItem: 'deleteWatchlistItem', enableAuthGoogle: 'enableAuthGoogle', + enableAuthOidc: 'enableAuthOidc', enableAuthToken: 'enableAuthToken', enableDataProviderGhostfolio: 'enableDataProviderGhostfolio', enableFearAndGreedIndex: 'enableFearAndGreedIndex', @@ -159,6 +160,7 @@ export function filterGlobalPermissions( return globalPermissions.filter((permission) => { return ( permission !== permissions.enableAuthGoogle && + permission !== permissions.enableAuthOidc && permission !== permissions.enableSubscription ); }); diff --git a/package-lock.json b/package-lock.json index 6a3829a1d..faa39e722 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,6 +83,7 @@ "passport-google-oauth20": "2.0.0", "passport-headerapikey": "1.2.2", "passport-jwt": "4.0.1", + "passport-openidconnect": "0.1.2", "reflect-metadata": "0.2.2", "rxjs": "7.8.1", "stripe": "18.5.0", @@ -129,6 +130,7 @@ "@types/node": "22.15.17", "@types/papaparse": "5.3.7", "@types/passport-google-oauth20": "2.0.16", + "@types/passport-openidconnect": "0.1.3", "@typescript-eslint/eslint-plugin": "8.43.0", "@typescript-eslint/parser": "8.43.0", "eslint": "9.35.0", @@ -14502,6 +14504,30 @@ "@types/passport": "*" } }, + "node_modules/@types/passport-openidconnect": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@types/passport-openidconnect/-/passport-openidconnect-0.1.3.tgz", + "integrity": "sha512-k1Ni7bG/9OZNo2Qpjg2W6GajL+pww6ZPaNWMXfpteCX4dXf4QgaZLt2hjR5IiPrqwBT9+W8KjCTJ/uhGIoBx/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -34723,6 +34749,23 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-openidconnect": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.2.tgz", + "integrity": "sha512-JX3rTyW+KFZ/E9OF/IpXJPbyLO9vGzcmXB5FgSP2jfL3LGKJPdV7zUE8rWeKeeI/iueQggOeFa3onrCmhxXZTg==", + "license": "MIT", + "dependencies": { + "oauth": "0.10.x", + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", diff --git a/package.json b/package.json index 5dae615b8..843091424 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "passport-google-oauth20": "2.0.0", "passport-headerapikey": "1.2.2", "passport-jwt": "4.0.1", + "passport-openidconnect": "0.1.2", "reflect-metadata": "0.2.2", "rxjs": "7.8.1", "stripe": "18.5.0", @@ -173,6 +174,7 @@ "@types/node": "22.15.17", "@types/papaparse": "5.3.7", "@types/passport-google-oauth20": "2.0.16", + "@types/passport-openidconnect": "0.1.3", "@typescript-eslint/eslint-plugin": "8.43.0", "@typescript-eslint/parser": "8.43.0", "eslint": "9.35.0", diff --git a/prisma/migrations/20251103162035_added_oidc_to_provider/migration.sql b/prisma/migrations/20251103162035_added_oidc_to_provider/migration.sql new file mode 100644 index 000000000..f71f6eded --- /dev/null +++ b/prisma/migrations/20251103162035_added_oidc_to_provider/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "Provider" ADD VALUE 'OIDC'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 72ec79008..232dde9ca 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -335,6 +335,7 @@ enum Provider { ANONYMOUS GOOGLE INTERNET_IDENTITY + OIDC } enum Role {