Browse Source

Feature/OIDC authentication (#5981)

* Set up OIDC authentication

* Update changelog
pull/6041/head
Germán Martín 2 days ago
committed by GitHub
parent
commit
4cd16c33f8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      CHANGELOG.md
  2. 48
      apps/api/src/app/auth/auth.controller.ts
  3. 82
      apps/api/src/app/auth/auth.module.ts
  4. 14
      apps/api/src/app/auth/auth.service.ts
  5. 19
      apps/api/src/app/auth/interfaces/interfaces.ts
  6. 114
      apps/api/src/app/auth/oidc-state.store.ts
  7. 69
      apps/api/src/app/auth/oidc.strategy.ts
  8. 4
      apps/api/src/app/info/info.service.ts
  9. 26
      apps/api/src/services/configuration/configuration.service.ts
  10. 9
      apps/api/src/services/interfaces/environment.interface.ts
  11. 7
      apps/client/src/app/components/header/header.component.ts
  12. 1
      apps/client/src/app/components/login-with-access-token-dialog/interfaces/interfaces.ts
  13. 11
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html
  14. 2
      libs/common/src/lib/permissions.ts
  15. 43
      package-lock.json
  16. 2
      package.json
  17. 2
      prisma/migrations/20251103162035_added_oidc_to_provider/migration.sql
  18. 1
      prisma/schema.prisma

5
CHANGELOG.md

@ -7,12 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
#### Added ### Added
- Introduced data source transformation support in the import functionality for self-hosted environments - 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 - 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 - Increased the numerical precision for cryptocurrency quantities in the holding detail dialog
- Upgraded `envalid` from version `8.1.0` to `8.1.1` - Upgraded `envalid` from version `8.1.0` to `8.1.1`

48
apps/api/src/app/auth/auth.controller.ts

@ -84,7 +84,6 @@ export class AuthController {
@Req() request: Request, @Req() request: Request,
@Res() response: Response @Res() response: Response
) { ) {
// Handles the Google OAuth2 callback
const jwt: string = (request.user as any).jwt; const jwt: string = (request.user as any).jwt;
if (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') @Get('webauthn/generate-registration-options')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async generateRegistrationOptions() { public async generateRegistrationOptions() {
@ -116,13 +155,6 @@ export class AuthController {
return this.webAuthService.verifyAttestation(body.credential); 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') @Post('webauthn/verify-authentication')
public async verifyAuthentication( public async verifyAuthentication(
@Body() body: { deviceId: string; credential: AssertionCredentialJSON } @Body() body: { deviceId: string; credential: AssertionCredentialJSON }

82
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 { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ApiKeyService } from '@ghostfolio/api/services/api-key/api-key.service'; import { ApiKeyService } from '@ghostfolio/api/services/api-key/api-key.service';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; 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 { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.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 { JwtModule } from '@nestjs/jwt';
import type { StrategyOptions } from 'passport-openidconnect';
import { ApiKeyStrategy } from './api-key.strategy'; import { ApiKeyStrategy } from './api-key.strategy';
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { GoogleStrategy } from './google.strategy'; import { GoogleStrategy } from './google.strategy';
import { JwtStrategy } from './jwt.strategy'; import { JwtStrategy } from './jwt.strategy';
import { OidcStrategy } from './oidc.strategy';
@Module({ @Module({
controllers: [AuthController], controllers: [AuthController],
@ -36,6 +39,83 @@ import { JwtStrategy } from './jwt.strategy';
AuthService, AuthService,
GoogleStrategy, GoogleStrategy,
JwtStrategy, 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 WebAuthService
] ]
}) })

14
apps/api/src/app/auth/auth.service.ts

@ -17,8 +17,6 @@ export class AuthService {
) {} ) {}
public async validateAnonymousLogin(accessToken: string): Promise<string> { public async validateAnonymousLogin(accessToken: string): Promise<string> {
return new Promise(async (resolve, reject) => {
try {
const hashedAccessToken = this.userService.createAccessToken({ const hashedAccessToken = this.userService.createAccessToken({
password: accessToken, password: accessToken,
salt: this.configurationService.get('ACCESS_TOKEN_SALT') salt: this.configurationService.get('ACCESS_TOKEN_SALT')
@ -29,19 +27,13 @@ export class AuthService {
}); });
if (user) { if (user) {
const jwt = this.jwtService.sign({ return this.jwtService.sign({
id: user.id id: user.id
}); });
}
resolve(jwt);
} else {
throw new Error(); throw new Error();
} }
} catch {
reject();
}
});
}
public async validateOAuthLogin({ public async validateOAuthLogin({
provider, provider,
@ -75,7 +67,7 @@ export class AuthService {
} catch (error) { } catch (error) {
throw new InternalServerErrorException( throw new InternalServerErrorException(
'validateOAuthLogin', 'validateOAuthLogin',
error.message error instanceof Error ? error.message : 'Unknown error'
); );
} }
} }

19
apps/api/src/app/auth/interfaces/interfaces.ts

@ -6,6 +6,25 @@ export interface AuthDeviceDialogParams {
authDevice: AuthDeviceDto; 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 { export interface ValidateOAuthLoginParams {
provider: Provider; provider: Provider;
thirdPartyId: string; thirdPartyId: string;

114
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)
);
}
}

69
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;
}
}
}

4
apps/api/src/app/info/info.service.ts

@ -55,6 +55,10 @@ export class InfoService {
globalPermissions.push(permissions.enableAuthGoogle); globalPermissions.push(permissions.enableAuthGoogle);
} }
if (this.configurationService.get('ENABLE_FEATURE_AUTH_OIDC')) {
globalPermissions.push(permissions.enableAuthOidc);
}
if (this.configurationService.get('ENABLE_FEATURE_AUTH_TOKEN')) { if (this.configurationService.get('ENABLE_FEATURE_AUTH_TOKEN')) {
globalPermissions.push(permissions.enableAuthToken); globalPermissions.push(permissions.enableAuthToken);
} }

26
apps/api/src/services/configuration/configuration.service.ts

@ -41,6 +41,7 @@ export class ConfigurationService {
default: [] default: []
}), }),
ENABLE_FEATURE_AUTH_GOOGLE: bool({ default: false }), ENABLE_FEATURE_AUTH_GOOGLE: bool({ default: false }),
ENABLE_FEATURE_AUTH_OIDC: bool({ default: false }),
ENABLE_FEATURE_AUTH_TOKEN: bool({ default: true }), ENABLE_FEATURE_AUTH_TOKEN: bool({ default: true }),
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }), ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES: bool({ default: true }), ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES: bool({ default: true }),
@ -54,9 +55,32 @@ export class ConfigurationService {
GOOGLE_SHEETS_ID: str({ default: '' }), GOOGLE_SHEETS_ID: str({ default: '' }),
GOOGLE_SHEETS_PRIVATE_KEY: str({ default: '' }), GOOGLE_SHEETS_PRIVATE_KEY: str({ default: '' }),
HOST: host({ default: DEFAULT_HOST }), HOST: host({ default: DEFAULT_HOST }),
JWT_SECRET_KEY: str({}), JWT_SECRET_KEY: str(),
MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }), MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
MAX_CHART_ITEMS: num({ default: 365 }), 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 }), PORT: port({ default: DEFAULT_PORT }),
PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: num({ PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: num({
default: DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY default: DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY

9
apps/api/src/services/interfaces/environment.interface.ts

@ -17,6 +17,7 @@ export interface Environment extends CleanedEnvAccessors {
DATA_SOURCES: string[]; DATA_SOURCES: string[];
DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER: string[]; DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER: string[];
ENABLE_FEATURE_AUTH_GOOGLE: boolean; ENABLE_FEATURE_AUTH_GOOGLE: boolean;
ENABLE_FEATURE_AUTH_OIDC: boolean;
ENABLE_FEATURE_AUTH_TOKEN: boolean; ENABLE_FEATURE_AUTH_TOKEN: boolean;
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean; ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;
ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES: boolean; ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES: boolean;
@ -32,6 +33,14 @@ export interface Environment extends CleanedEnvAccessors {
JWT_SECRET_KEY: string; JWT_SECRET_KEY: string;
MAX_ACTIVITIES_TO_IMPORT: number; MAX_ACTIVITIES_TO_IMPORT: number;
MAX_CHART_ITEMS: 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; PORT: number;
PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: number; PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: number;
PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY: number; PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY: number;

7
apps/client/src/app/components/header/header.component.ts

@ -105,6 +105,7 @@ export class GfHeaderComponent implements OnChanges {
public hasFilters: boolean; public hasFilters: boolean;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public hasPermissionForAuthGoogle: boolean; public hasPermissionForAuthGoogle: boolean;
public hasPermissionForAuthOidc: boolean;
public hasPermissionForAuthToken: boolean; public hasPermissionForAuthToken: boolean;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public hasPermissionToAccessAdminControl: boolean; public hasPermissionToAccessAdminControl: boolean;
@ -170,6 +171,11 @@ export class GfHeaderComponent implements OnChanges {
permissions.enableAuthGoogle permissions.enableAuthGoogle
); );
this.hasPermissionForAuthOidc = hasPermission(
this.info?.globalPermissions,
permissions.enableAuthOidc
);
this.hasPermissionForAuthToken = hasPermission( this.hasPermissionForAuthToken = hasPermission(
this.info?.globalPermissions, this.info?.globalPermissions,
permissions.enableAuthToken permissions.enableAuthToken
@ -286,6 +292,7 @@ export class GfHeaderComponent implements OnChanges {
data: { data: {
accessToken: '', accessToken: '',
hasPermissionToUseAuthGoogle: this.hasPermissionForAuthGoogle, hasPermissionToUseAuthGoogle: this.hasPermissionForAuthGoogle,
hasPermissionToUseAuthOidc: this.hasPermissionForAuthOidc,
hasPermissionToUseAuthToken: this.hasPermissionForAuthToken, hasPermissionToUseAuthToken: this.hasPermissionForAuthToken,
title: $localize`Sign in` title: $localize`Sign in`
}, },

1
apps/client/src/app/components/login-with-access-token-dialog/interfaces/interfaces.ts

@ -1,6 +1,7 @@
export interface LoginWithAccessTokenDialogParams { export interface LoginWithAccessTokenDialogParams {
accessToken: string; accessToken: string;
hasPermissionToUseAuthGoogle: boolean; hasPermissionToUseAuthGoogle: boolean;
hasPermissionToUseAuthOidc: boolean;
hasPermissionToUseAuthToken: boolean; hasPermissionToUseAuthToken: boolean;
title: string; title: string;
} }

11
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html

@ -45,6 +45,17 @@
> >
</div> </div>
} }
@if (data.hasPermissionToUseAuthOidc) {
<div class="d-flex flex-column mt-2">
<a
class="px-4 rounded-pill"
href="../api/auth/oidc"
mat-stroked-button
><ng-container i18n>Sign in with OIDC</ng-container></a
>
</div>
}
</form> </form>
</div> </div>
</div> </div>

2
libs/common/src/lib/permissions.ts

@ -29,6 +29,7 @@ export const permissions = {
deleteUser: 'deleteUser', deleteUser: 'deleteUser',
deleteWatchlistItem: 'deleteWatchlistItem', deleteWatchlistItem: 'deleteWatchlistItem',
enableAuthGoogle: 'enableAuthGoogle', enableAuthGoogle: 'enableAuthGoogle',
enableAuthOidc: 'enableAuthOidc',
enableAuthToken: 'enableAuthToken', enableAuthToken: 'enableAuthToken',
enableDataProviderGhostfolio: 'enableDataProviderGhostfolio', enableDataProviderGhostfolio: 'enableDataProviderGhostfolio',
enableFearAndGreedIndex: 'enableFearAndGreedIndex', enableFearAndGreedIndex: 'enableFearAndGreedIndex',
@ -159,6 +160,7 @@ export function filterGlobalPermissions(
return globalPermissions.filter((permission) => { return globalPermissions.filter((permission) => {
return ( return (
permission !== permissions.enableAuthGoogle && permission !== permissions.enableAuthGoogle &&
permission !== permissions.enableAuthOidc &&
permission !== permissions.enableSubscription permission !== permissions.enableSubscription
); );
}); });

43
package-lock.json

@ -83,6 +83,7 @@
"passport-google-oauth20": "2.0.0", "passport-google-oauth20": "2.0.0",
"passport-headerapikey": "1.2.2", "passport-headerapikey": "1.2.2",
"passport-jwt": "4.0.1", "passport-jwt": "4.0.1",
"passport-openidconnect": "0.1.2",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"stripe": "18.5.0", "stripe": "18.5.0",
@ -129,6 +130,7 @@
"@types/node": "22.15.17", "@types/node": "22.15.17",
"@types/papaparse": "5.3.7", "@types/papaparse": "5.3.7",
"@types/passport-google-oauth20": "2.0.16", "@types/passport-google-oauth20": "2.0.16",
"@types/passport-openidconnect": "0.1.3",
"@typescript-eslint/eslint-plugin": "8.43.0", "@typescript-eslint/eslint-plugin": "8.43.0",
"@typescript-eslint/parser": "8.43.0", "@typescript-eslint/parser": "8.43.0",
"eslint": "9.35.0", "eslint": "9.35.0",
@ -14502,6 +14504,30 @@
"@types/passport": "*" "@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": { "node_modules/@types/qs": {
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
@ -34723,6 +34749,23 @@
"url": "https://github.com/sponsors/jaredhanson" "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": { "node_modules/passport-strategy": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",

2
package.json

@ -127,6 +127,7 @@
"passport-google-oauth20": "2.0.0", "passport-google-oauth20": "2.0.0",
"passport-headerapikey": "1.2.2", "passport-headerapikey": "1.2.2",
"passport-jwt": "4.0.1", "passport-jwt": "4.0.1",
"passport-openidconnect": "0.1.2",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"stripe": "18.5.0", "stripe": "18.5.0",
@ -173,6 +174,7 @@
"@types/node": "22.15.17", "@types/node": "22.15.17",
"@types/papaparse": "5.3.7", "@types/papaparse": "5.3.7",
"@types/passport-google-oauth20": "2.0.16", "@types/passport-google-oauth20": "2.0.16",
"@types/passport-openidconnect": "0.1.3",
"@typescript-eslint/eslint-plugin": "8.43.0", "@typescript-eslint/eslint-plugin": "8.43.0",
"@typescript-eslint/parser": "8.43.0", "@typescript-eslint/parser": "8.43.0",
"eslint": "9.35.0", "eslint": "9.35.0",

2
prisma/migrations/20251103162035_added_oidc_to_provider/migration.sql

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "Provider" ADD VALUE 'OIDC';

1
prisma/schema.prisma

@ -335,6 +335,7 @@ enum Provider {
ANONYMOUS ANONYMOUS
GOOGLE GOOGLE
INTERNET_IDENTITY INTERNET_IDENTITY
OIDC
} }
enum Role { enum Role {

Loading…
Cancel
Save