From 62975359210f5a90d220ccf287020d709237808b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADn?= Date: Mon, 3 Nov 2025 21:55:25 +0100 Subject: [PATCH] feat(auth): add OIDC authentication support - Implemented OIDC login and callback endpoints in AuthController. - Created OidcStrategy for handling OIDC authentication logic. - Introduced OidcStateStore for managing OIDC state parameters in memory. - Updated AuthModule to include OidcStrategy and its dependencies. - Enhanced ConfigurationService to support OIDC-related configurations. - Modified InfoService to expose enabled social login providers. - Updated client components to support OIDC login option. - Added OIDC provider to the Prisma schema and migration. --- .env.example | 28 + OIDC_IMPLEMENTATION_PLAN.md | 814 ++++++++++++++++++ apps/api/src/app/auth/auth.controller.ts | 47 + apps/api/src/app/auth/auth.module.ts | 42 + apps/api/src/app/auth/oidc-state.store.ts | 111 +++ apps/api/src/app/auth/oidc.strategy.ts | 123 +++ apps/api/src/app/info/info.service.ts | 18 + .../configuration/configuration.service.ts | 9 + .../interfaces/environment.interface.ts | 9 + .../app/components/header/header.component.ts | 1 + .../interfaces/interfaces.ts | 1 + .../login-with-access-token-dialog.html | 33 +- .../src/app/pages/register/register-page.html | 33 +- .../src/lib/interfaces/info-item.interface.ts | 1 + package-lock.json | 43 + package.json | 2 + .../migration.sql | 2 + prisma/schema.prisma | 1 + 18 files changed, 1297 insertions(+), 21 deletions(-) create mode 100644 OIDC_IMPLEMENTATION_PLAN.md create mode 100644 apps/api/src/app/auth/oidc-state.store.ts create mode 100644 apps/api/src/app/auth/oidc.strategy.ts create mode 100644 prisma/migrations/20251103162035_add_oidc_provider/migration.sql diff --git a/.env.example b/.env.example index e4a935626..7b090a046 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,31 @@ POSTGRES_PASSWORD= ACCESS_TOKEN_SALT= DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer JWT_SECRET_KEY= + +ROOT_URL=https:// + +# Enable social login (Google, OIDC, etc.) +# ENABLE_FEATURE_SOCIAL_LOGIN=true + +# OIDC AUTHENTICATION (Optional) +# Enable/disable OIDC authentication +OIDC_ENABLED=false +# OIDC Issuer URL (with trailing slash, must be HTTPS in production) +# Examples: +# - Keycloak: https://your-keycloak.com/realms/your-realm/ +# - Auth0: https://your-tenant.auth0.com/ +# - Authentik: https://your-authentik.com/application/o/app-name/ +# - Azure AD: https://login.microsoftonline.com/your-tenant-id/v2.0/ +OIDC_ISSUER=https://your-oidc-provider.com/ +# OAuth 2.0 Client credentials +OIDC_CLIENT_ID= +OIDC_CLIENT_SECRET= +# Callback URL (where OIDC provider redirects after authentication) +OIDC_CALLBACK_URL=${ROOT_URL}/api/auth/oidc/callback +# OpenID Connect scopes (space-separated) +OIDC_SCOPE=openid profile email +# Optional: Override default endpoints (auto-constructed from issuer if not set) +# Only set these if your provider has non-standard endpoint paths +# OIDC_AUTHORIZATION_URL=https://your-oidc-provider.com/authorize +# OIDC_TOKEN_URL=https://your-oidc-provider.com/token +# OIDC_USER_INFO_URL=https://your-oidc-provider.com/userinfo diff --git a/OIDC_IMPLEMENTATION_PLAN.md b/OIDC_IMPLEMENTATION_PLAN.md new file mode 100644 index 000000000..d8bf612e2 --- /dev/null +++ b/OIDC_IMPLEMENTATION_PLAN.md @@ -0,0 +1,814 @@ +# Plan de Implementación: Autenticación OIDC en Ghostfolio + +**Fecha de inicio**: 3 de noviembre de 2025 +**Estado**: 🟡 En planificación + +--- + +## 📋 Análisis de Autenticación Actual + +### 🔐 Métodos de Autenticación Existentes + +#### 1. **Autenticación Anónima (Access Token)** + +- ✅ **Ubicación**: `apps/api/src/app/auth/auth.controller.ts` y `auth.service.ts` +- **Funcionamiento**: + - Los usuarios crean una cuenta y reciben un `accessToken` + - El token se hashea con salt y se valida contra la base de datos + - Genera un JWT tras validación exitosa +- **Endpoints**: + - `POST /api/auth/anonymous` (actual) + - `GET /api/auth/anonymous/:accessToken` (deprecated) + +#### 2. **OAuth 2.0 con Google** + +- ✅ **Ubicación**: `apps/api/src/app/auth/google.strategy.ts` +- **Implementación**: + - Usa `passport-google-oauth20` + - Flujo estándar OAuth 2.0 + - Callback URL: `/api/auth/google/callback` + - Scope: `['profile']` (solo perfil básico) +- **Variables de entorno**: + - `GOOGLE_CLIENT_ID` + - `GOOGLE_SECRET` + - `ROOT_URL` + +#### 3. **WebAuthn (Autenticación sin contraseña)** + +- ✅ **Ubicación**: `apps/api/src/app/auth/web-auth.service.ts` +- **Implementación**: + - Usa `@simplewebauthn/server` y `@simplewebauthn/browser` + - Soporte para dispositivos biométricos (Face ID, Touch ID, etc.) + - Registro y autenticación de dispositivos +- **Endpoints**: + - `GET /api/auth/webauthn/generate-registration-options` + - `POST /api/auth/webauthn/verify-attestation` + - `POST /api/auth/webauthn/generate-authentication-options` + - `POST /api/auth/webauthn/verify-authentication` + +#### 4. **API Key Authentication** + +- ✅ **Ubicación**: `apps/api/src/app/auth/api-key.strategy.ts` +- **Funcionamiento**: + - Usa `passport-headerapikey` + - Header: `x-api-key` con prefijo `Api-Key` + - Para integración con APIs externas + +#### 5. **JWT Bearer Token** + +- ✅ **Ubicación**: `apps/api/src/app/auth/jwt.strategy.ts` +- **Implementación**: + - Estrategia principal de autorización + - Expira en 180 días + - Extracción desde Authorization header + - Validación de usuarios inactivos y analytics + +### 📊 Modelo de Datos Actual (Prisma) + +```prisma +enum Provider { + ANONYMOUS + GOOGLE + INTERNET_IDENTITY // No implementado actualmente +} + +model User { + // ... otros campos + provider Provider @default(ANONYMOUS) + thirdPartyId String? + // ... +} +``` + +--- + +## 🎯 Plan de Implementación OIDC + +### **Fase 1: Análisis y Diseño** ✅ + +- [x] Analizar métodos de autenticación existentes +- [x] Diseñar arquitectura de integración OIDC +- [x] Definir enfoque de coexistencia con métodos actuales +- [x] Decidir estrategia de provider genérico vs específico + +**Decisiones de diseño**: + +1. Estrategia genérica OIDC configurable para múltiples proveedores +2. OIDC coexistirá con los métodos existentes +3. Añadir nuevo valor `OIDC` al enum Provider +4. Usar campo `thirdPartyId` existente para almacenar `sub` del token OIDC + +--- + +### **Fase 2: Cambios en la Base de Datos** ⏳ + +- [ ] Modificar `prisma/schema.prisma` para añadir Provider.OIDC +- [ ] (Opcional) Añadir campo `oidcIssuer` para distinguir proveedores +- [ ] Crear migración de base de datos +- [ ] Ejecutar migración en desarrollo +- [ ] Validar cambios en base de datos + +#### Cambios necesarios en `prisma/schema.prisma` + +```prisma +enum Provider { + ANONYMOUS + GOOGLE + INTERNET_IDENTITY + OIDC // 🆕 Añadir +} + +// Opcional: Si se necesita distinguir múltiples proveedores OIDC +model User { + // ... campos existentes + oidcIssuer String? // 🆕 Para identificar el proveedor OIDC específico +} +``` + +#### Comandos a ejecutar + +```bash +# Crear migración +npx prisma migrate dev --name add_oidc_provider + +# O push directo para desarrollo +npm run database:push +``` + +**Archivos afectados**: + +- `prisma/schema.prisma` +- `prisma/migrations/YYYYMMDD_add_oidc_provider/migration.sql` (nuevo) + +--- + +### **Fase 3: Dependencias** ⏳ + +- [ ] Instalar `passport-openidconnect` +- [ ] Instalar `@types/passport-openidconnect` +- [ ] Verificar compatibilidad de versiones +- [ ] Actualizar `package.json` y `package-lock.json` + +#### Comandos a ejecutar + +```bash +npm install passport-openidconnect +npm install --save-dev @types/passport-openidconnect +``` + +**Archivos afectados**: + +- `package.json` +- `package-lock.json` + +--- + +### **Fase 4: Variables de Entorno** ⏳ + +- [ ] Actualizar `.env.example` con variables OIDC +- [ ] Documentar cada variable de entorno +- [ ] Crear configuración de ejemplo para proveedores comunes +- [ ] Añadir variables al ConfigurationService si es necesario + +#### Añadir a `.env.example` + +```bash +# ==================================== +# OIDC Authentication Configuration +# ==================================== +# Enable/disable OIDC authentication +OIDC_ENABLED=false + +# OIDC Provider base URL (must be HTTPS in production) +OIDC_ISSUER=https://your-oidc-provider.com + +# OAuth 2.0 Client credentials +OIDC_CLIENT_ID= +OIDC_CLIENT_SECRET= + +# Callback URL (where OIDC provider redirects after authentication) +OIDC_CALLBACK_URL=${ROOT_URL}/api/auth/oidc/callback + +# OpenID Connect scopes (space-separated) +OIDC_SCOPE=openid profile email + +# Optional: Override default endpoints (auto-discovered from issuer if not set) +# OIDC_AUTHORIZATION_URL=https://your-oidc-provider.com/authorize +# OIDC_TOKEN_URL=https://your-oidc-provider.com/token +# OIDC_USER_INFO_URL=https://your-oidc-provider.com/userinfo +``` + +#### Ejemplos de configuración por proveedor + +
+Keycloak + +```bash +OIDC_ENABLED=true +OIDC_ISSUER=https://keycloak.example.com/realms/your-realm +OIDC_CLIENT_ID=ghostfolio +OIDC_CLIENT_SECRET=your-secret-here +OIDC_SCOPE=openid profile email +``` + +
+ +
+Auth0 + +```bash +OIDC_ENABLED=true +OIDC_ISSUER=https://your-tenant.auth0.com +OIDC_CLIENT_ID=your-client-id +OIDC_CLIENT_SECRET=your-secret-here +OIDC_SCOPE=openid profile email +``` + +
+ +
+Azure AD + +```bash +OIDC_ENABLED=true +OIDC_ISSUER=https://login.microsoftonline.com/{tenant-id}/v2.0 +OIDC_CLIENT_ID=your-application-id +OIDC_CLIENT_SECRET=your-client-secret +OIDC_SCOPE=openid profile email +``` + +
+ +
+Okta + +```bash +OIDC_ENABLED=true +OIDC_ISSUER=https://your-domain.okta.com +OIDC_CLIENT_ID=your-client-id +OIDC_CLIENT_SECRET=your-client-secret +OIDC_SCOPE=openid profile email +``` + +
+ +**Archivos afectados**: + +- `.env.example` +- (Posiblemente) `apps/api/src/services/configuration/configuration.service.ts` + +--- + +### **Fase 5: Implementación Backend** ⏳ + +#### 5.1. Crear estrategia OIDC ⏳ + +- [ ] Crear archivo `apps/api/src/app/auth/oidc.strategy.ts` +- [ ] Implementar clase OidcStrategy extendiendo PassportStrategy +- [ ] Configurar discovery automático de endpoints +- [ ] Implementar método validate +- [ ] Manejar errores y logging +- [ ] Añadir validación de configuración + +**Archivo a crear**: `apps/api/src/app/auth/oidc.strategy.ts` + +```typescript +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, VerifyCallback } from 'passport-openidconnect'; +import { AuthService } from './auth.service'; + +@Injectable() +export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') { + private static readonly logger = new Logger(OidcStrategy.name); + + 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 enabled = configurationService.get('OIDC_ENABLED') === 'true'; + + if (!enabled || !issuer || !clientID || !clientSecret) { + OidcStrategy.logger.warn( + 'OIDC authentication is not configured or disabled. ' + + 'Set OIDC_ENABLED=true and provide OIDC_ISSUER, OIDC_CLIENT_ID, and OIDC_CLIENT_SECRET.' + ); + // Passport requiere configuración válida, usar placeholders + super({ + issuer: 'https://placeholder.example.com', + clientID: 'placeholder', + clientSecret: 'placeholder', + authorizationURL: 'https://placeholder.example.com/authorize', + tokenURL: 'https://placeholder.example.com/token', + userInfoURL: 'https://placeholder.example.com/userinfo', + callbackURL: 'https://placeholder.example.com/callback', + scope: 'openid profile' + }); + return; + } + + const callbackURL = configurationService.get('OIDC_CALLBACK_URL') || + `${configurationService.get('ROOT_URL')}/api/auth/oidc/callback`; + + const scope = configurationService.get('OIDC_SCOPE') || 'openid profile email'; + + // Los endpoints se descubren automáticamente desde el issuer si no se proporcionan + const config: any = { + issuer, + clientID, + clientSecret, + callbackURL, + scope: scope.split(' '), + passReqToCallback: true + }; + + // Endpoints opcionales (si no se proporcionan, se descubren automáticamente) + const authorizationURL = configurationService.get('OIDC_AUTHORIZATION_URL'); + const tokenURL = configurationService.get('OIDC_TOKEN_URL'); + const userInfoURL = configurationService.get('OIDC_USER_INFO_URL'); + + if (authorizationURL) config.authorizationURL = authorizationURL; + if (tokenURL) config.tokenURL = tokenURL; + if (userInfoURL) config.userInfoURL = userInfoURL; + + super(config); + + OidcStrategy.logger.log( + `OIDC authentication configured with issuer: ${issuer}` + ); + } + + public async validate( + _request: any, + _issuer: string, + profile: any, + _context: any, + _idToken: any, + _accessToken: any, + _refreshToken: any, + params: any, + done: VerifyCallback + ) { + try { + // El 'sub' (subject) del token es el identificador único del usuario + const thirdPartyId = params.sub || profile.id; + + if (!thirdPartyId) { + throw new Error('No subject (sub) found in OIDC token'); + } + + OidcStrategy.logger.debug( + `Validating OIDC user with sub: ${thirdPartyId}` + ); + + const jwt = await this.authService.validateOAuthLogin({ + provider: Provider.OIDC, + thirdPartyId + }); + + done(null, { jwt, profile }); + } catch (error) { + OidcStrategy.logger.error( + `OIDC validation error: ${error.message}`, + error.stack + ); + done(error, false); + } + } +} +``` + +#### 5.2. Actualizar módulo de autenticación ⏳ + +- [ ] Importar OidcStrategy en `auth.module.ts` +- [ ] Añadir OidcStrategy a providers +- [ ] Verificar importaciones + +**Archivo a modificar**: `apps/api/src/app/auth/auth.module.ts` + +Añadir: + +```typescript +import { OidcStrategy } from './oidc.strategy'; + +@Module({ + // ... + providers: [ + ApiKeyService, + ApiKeyStrategy, + AuthDeviceService, + AuthService, + GoogleStrategy, + JwtStrategy, + OidcStrategy, // 🆕 Añadir aquí + WebAuthService + ] +}) +export class AuthModule {} +``` + +#### 5.3. Añadir endpoints al controlador ⏳ + +- [ ] Añadir endpoint `/auth/oidc` para iniciar login +- [ ] Añadir endpoint `/auth/oidc/callback` para callback +- [ ] Implementar manejo de errores +- [ ] Añadir guard condicional si OIDC está deshabilitado + +**Archivo a modificar**: `apps/api/src/app/auth/auth.controller.ts` + +Añadir antes del último endpoint: + +```typescript +@Get('oidc') +@UseGuards(AuthGuard('oidc')) +public oidcLogin() { + // Inicia el flujo de autenticación OIDC + // Passport redirige automáticamente al proveedor OIDC +} + +@Get('oidc/callback') +@UseGuards(AuthGuard('oidc')) +@Version(VERSION_NEUTRAL) +public oidcLoginCallback( + @Req() request: Request, + @Res() response: Response +) { + // Maneja el callback del proveedor OIDC + const jwt: string = (request.user as any).jwt; + + if (jwt) { + response.redirect( + `${this.configurationService.get('ROOT_URL')}/${DEFAULT_LANGUAGE_CODE}/auth/${jwt}` + ); + } else { + // Error en autenticación + response.redirect( + `${this.configurationService.get('ROOT_URL')}/${DEFAULT_LANGUAGE_CODE}/auth?error=oidc_failed` + ); + } +} +``` + +#### 5.4. Verificar AuthService ⏳ + +- [ ] Confirmar que `validateOAuthLogin` funciona con Provider.OIDC +- [ ] Añadir lógica específica si es necesario (ej: almacenar issuer) +- [ ] Verificar creación de usuarios nuevos +- [ ] Verificar que property `isUserSignupEnabled` se respeta + +**Archivo a revisar**: `apps/api/src/app/auth/auth.service.ts` + +El método existente ya debería funcionar: + +```typescript +public async validateOAuthLogin({ + provider, + thirdPartyId +}: ValidateOAuthLoginParams): Promise { + // Este método ya soporta cualquier provider del enum + // No requiere cambios si Provider.OIDC está en el enum +} +``` + +#### 5.5. Testing backend ⏳ + +- [ ] Crear tests unitarios para OidcStrategy +- [ ] Crear tests de integración para endpoints +- [ ] Verificar manejo de errores +- [ ] Test con configuración inválida/faltante + +**Archivos a crear**: + +- `apps/api/src/app/auth/oidc.strategy.spec.ts` + +--- + +### **Fase 6: Implementación Frontend** ⏳ + +#### 6.1. Añadir botón de login OIDC ⏳ + +- [ ] Identificar componente de login actual +- [ ] Añadir botón "Sign in with OIDC" o customizable +- [ ] Implementar redirección a `/api/auth/oidc` +- [ ] Añadir iconografía apropiada +- [ ] Hacer el botón condicional (mostrar solo si OIDC está habilitado) + +**Archivos a investigar**: + +- `apps/client/src/app/pages/landing/` +- `apps/client/src/app/pages/webauthn/` +- Componentes relacionados con login/auth + +#### 6.2. Verificar flujo de redirección ⏳ + +- [ ] Confirmar que `/auth/:jwt` maneja correctamente el token OIDC +- [ ] Añadir manejo de errores (parámetro `?error=oidc_failed`) +- [ ] Verificar storage del token +- [ ] Probar navegación post-login + +**Archivo a revisar**: `apps/client/src/app/pages/auth/auth-page.component.ts` + +#### 6.3. Configuración visual (opcional) ⏳ + +- [ ] Permitir customización del texto del botón via config +- [ ] Permitir customización del logo del proveedor +- [ ] Responsive design para botón OIDC + +#### 6.4. Testing frontend ⏳ + +- [ ] Test unitario del componente con botón OIDC +- [ ] Test e2e del flujo completo +- [ ] Verificar en diferentes navegadores + +--- + +### **Fase 7: Configuración y Seguridad** ⏳ + +#### 7.1. Validaciones de seguridad ⏳ + +- [ ] Validar que OIDC_ISSUER sea HTTPS en producción +- [ ] Implementar validación de state/nonce +- [ ] Verificar validación de firma de tokens JWT +- [ ] Implementar timeout de sesión configurable +- [ ] Añadir rate limiting a endpoints OIDC + +#### 7.2. ConfigurationService ⏳ + +- [ ] Verificar que todas las variables OIDC están disponibles +- [ ] Añadir validación de variables requeridas al inicio +- [ ] Implementar feature flag para OIDC_ENABLED + +**Archivo a modificar (posiblemente)**: + +- `apps/api/src/services/configuration/configuration.service.ts` + +#### 7.3. Logging y monitoreo ⏳ + +- [ ] Añadir logs de eventos OIDC importantes +- [ ] Implementar métricas de autenticación OIDC +- [ ] Añadir alertas para fallos de autenticación + +#### 7.4. Multi-issuer (Opcional avanzado) ⏳ + +Si se necesita soportar múltiples proveedores OIDC simultáneamente: + +- [ ] Añadir campo `oidcIssuer` al modelo User +- [ ] Modificar `validateOAuthLogin` para incluir issuer +- [ ] Crear estrategias dinámicas por issuer +- [ ] Añadir UI para seleccionar proveedor + +--- + +### **Fase 8: Testing Integral** ⏳ + +#### 8.1. Testing local ⏳ + +- [ ] Configurar Keycloak local en Docker +- [ ] Probar flujo completo de registro nuevo usuario +- [ ] Probar flujo completo de login usuario existente +- [ ] Probar manejo de errores (credenciales inválidas) +- [ ] Probar con OIDC deshabilitado + +#### 8.2. Testing con proveedores reales ⏳ + +- [ ] Keycloak +- [ ] Auth0 +- [ ] Azure AD +- [ ] Okta +- [ ] Otro proveedor OIDC genérico + +#### 8.3. Testing de regresión ⏳ + +- [ ] Verificar que Google OAuth sigue funcionando +- [ ] Verificar que autenticación anónima sigue funcionando +- [ ] Verificar que WebAuthn sigue funcionando +- [ ] Verificar que API Keys siguen funcionando + +#### 8.4. Testing de seguridad ⏳ + +- [ ] Intentar bypass de autenticación +- [ ] Verificar protección CSRF +- [ ] Verificar manejo de tokens expirados +- [ ] Penetration testing básico + +--- + +### **Fase 9: Documentación** ⏳ + +#### 9.1. Documentación técnica ⏳ + +- [ ] Actualizar `DEVELOPMENT.md` con setup OIDC +- [ ] Documentar arquitectura de autenticación +- [ ] Crear diagrama de flujo OIDC +- [ ] Documentar variables de entorno + +**Archivo a modificar**: `DEVELOPMENT.md` + +Sección a añadir: + +````markdown +### OIDC Authentication Setup + +Ghostfolio supports OpenID Connect (OIDC) authentication with any compliant provider. + +#### Configuration + +1. Set up your OIDC provider (Keycloak, Auth0, Azure AD, Okta, etc.) +2. Register Ghostfolio as a client application +3. Configure the following environment variables in your `.env` file: + +```bash +OIDC_ENABLED=true +OIDC_ISSUER=https://your-provider.com +OIDC_CLIENT_ID=your-client-id +OIDC_CLIENT_SECRET=your-client-secret +OIDC_CALLBACK_URL=https://your-ghostfolio-instance.com/api/auth/oidc/callback +OIDC_SCOPE=openid profile email +``` +```` + +4. Restart the Ghostfolio server +5. Users can now authenticate using the "Sign in with OIDC" button + +#### Provider-Specific Setup Guides + +See [docs/oidc-providers.md](docs/oidc-providers.md) for detailed setup instructions for: + +- Keycloak +- Auth0 +- Azure Active Directory +- Okta +- Generic OIDC providers + +``` + +#### 9.2. Guías de configuración por proveedor ⏳ + +- [ ] Crear `docs/oidc-providers.md` con guías detalladas +- [ ] Incluir screenshots del proceso de configuración +- [ ] Documentar troubleshooting común por proveedor + +**Archivo a crear**: `docs/oidc-providers.md` + +#### 9.3. Documentación de usuario ⏳ + +- [ ] Actualizar FAQ sobre métodos de autenticación +- [ ] Crear guía de usuario para login con OIDC +- [ ] Documentar cómo migrar de un método a otro + +#### 9.4. Changelog ⏳ + +- [ ] Actualizar `CHANGELOG.md` con nueva feature OIDC +- [ ] Documentar breaking changes si los hay +- [ ] Listar proveedores OIDC probados + +**Archivo a modificar**: `CHANGELOG.md` + +--- + +### **Fase 10: Deployment y Rollout** ⏳ + +#### 10.1. Preparación para producción ⏳ + +- [ ] Crear checklist de deployment +- [ ] Documentar proceso de rollback +- [ ] Preparar scripts de migración de BD +- [ ] Configurar variables de entorno en producción + +#### 10.2. Deployment gradual ⏳ + +- [ ] Desplegar en entorno de staging +- [ ] Testing en staging con usuarios beta +- [ ] Desplegar en producción con feature flag deshabilitado +- [ ] Habilitar OIDC gradualmente + +#### 10.3. Monitoreo post-deployment ⏳ + +- [ ] Monitorear logs de errores OIDC +- [ ] Monitorear tasa de éxito de autenticación +- [ ] Recopilar feedback de usuarios +- [ ] Ajustar configuración según necesidad + +--- + +## 📊 Progreso General + +``` + +[██████░░░░░░░░░░░░░░] 30% - Fase 1 completada + +Fase 1: Análisis y Diseño ✅ 100% +Fase 2: Base de Datos ⏳ 0% +Fase 3: Dependencias ⏳ 0% +Fase 4: Variables de Entorno ⏳ 0% +Fase 5: Backend ⏳ 0% +Fase 6: Frontend ⏳ 0% +Fase 7: Seguridad ⏳ 0% +Fase 8: Testing ⏳ 0% +Fase 9: Documentación ⏳ 0% +Fase 10: Deployment ⏳ 0% + +``` + +--- + +## 📝 Archivos a Crear + +### Nuevos archivos backend: +- [ ] `apps/api/src/app/auth/oidc.strategy.ts` +- [ ] `apps/api/src/app/auth/oidc.strategy.spec.ts` + +### Nuevos archivos de documentación: +- [ ] `docs/oidc-providers.md` +- [x] `OIDC_IMPLEMENTATION_PLAN.md` (este archivo) + +--- + +## 📝 Archivos a Modificar + +### Backend: +- [ ] `prisma/schema.prisma` (añadir Provider.OIDC y opcionalmente oidcIssuer) +- [ ] `apps/api/src/app/auth/auth.module.ts` (registrar OidcStrategy) +- [ ] `apps/api/src/app/auth/auth.controller.ts` (añadir endpoints OIDC) +- [ ] `apps/api/src/app/auth/auth.service.ts` (verificar, posibles ajustes) +- [ ] `apps/api/src/services/configuration/configuration.service.ts` (posible) + +### Configuración: +- [ ] `.env.example` (añadir variables OIDC) +- [ ] `package.json` (nuevas dependencias) + +### Frontend: +- [ ] Componente de login (añadir botón OIDC) +- [ ] `apps/client/src/app/pages/auth/auth-page.component.ts` (manejo de errores) + +### Documentación: +- [ ] `DEVELOPMENT.md` (instrucciones de configuración OIDC) +- [ ] `CHANGELOG.md` (documentar nueva feature) +- [ ] `README.md` (mencionar soporte OIDC) + +--- + +## 🎯 Próximos Pasos Inmediatos + +1. **Comenzar Fase 2**: Modificar schema de Prisma +2. **Instalar dependencias**: passport-openidconnect +3. **Crear estrategia OIDC**: Implementar oidc.strategy.ts + +--- + +## 🔧 Consideraciones Técnicas + +### Ventajas de este enfoque: +✅ Mínima invasión en código existente +✅ Aprovecha arquitectura Passport ya establecida +✅ Flexible para cualquier proveedor OIDC +✅ Coexiste con métodos de autenticación actuales +✅ Fácil de mantener y extender +✅ Bien documentado y testeable + +### Posibles desafíos: +⚠️ Configuración puede ser compleja para usuarios finales +⚠️ Diferentes proveedores OIDC tienen quirks específicos +⚠️ Testing requiere configuración de provider de desarrollo +⚠️ Manejo de múltiples issuers añade complejidad + +### Alternativas consideradas: +- **OAuth2 específico por proveedor**: Más trabajo, menos flexible +- **Auth0/Keycloak como único proveedor**: Limita opciones de usuarios +- **Passport-oauth2 genérico**: OIDC es más estándar y específico + +--- + +## 📚 Referencias + +### Documentación relevante: +- [OpenID Connect Specification](https://openid.net/connect/) +- [Passport-OpenIDConnect Strategy](https://github.com/jaredhanson/passport-openidconnect) +- [NestJS Passport Integration](https://docs.nestjs.com/security/authentication) +- [Prisma Schema Reference](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference) + +### Proveedores OIDC para testing: +- [Keycloak (self-hosted)](https://www.keycloak.org/) +- [Auth0](https://auth0.com/) +- [Azure AD](https://azure.microsoft.com/en-us/services/active-directory/) +- [Okta](https://www.okta.com/) + +--- + +## 📞 Contacto y Soporte + +Para preguntas o problemas durante la implementación: +- Revisar logs en `apps/api/src/app/auth/oidc.strategy.ts` +- Verificar configuración de variables de entorno +- Consultar documentación del proveedor OIDC específico + +--- + +**Última actualización**: 3 de noviembre de 2025 +**Versión del plan**: 1.0 +``` diff --git a/apps/api/src/app/auth/auth.controller.ts b/apps/api/src/app/auth/auth.controller.ts index 57fd04bc7..074979d89 100644 --- a/apps/api/src/app/auth/auth.controller.ts +++ b/apps/api/src/app/auth/auth.controller.ts @@ -102,6 +102,53 @@ export class AuthController { } } + @Get('oidc') + @UseGuards(AuthGuard('oidc')) + @Version(VERSION_NEUTRAL) + public oidcLogin(@Res() response: Response) { + // Check if OIDC is enabled + const oidcEnabled = + this.configurationService.get('OIDC_ENABLED') === 'true'; + + if (!oidcEnabled) { + response.status(404).send('OIDC authentication is not enabled'); + return; + } + + // Initiates the OIDC login flow + } + + @Get('oidc/callback') + @UseGuards(AuthGuard('oidc')) + @Version(VERSION_NEUTRAL) + public oidcLoginCallback(@Req() request: Request, @Res() response: Response) { + // Check if OIDC is enabled + const oidcEnabled = + this.configurationService.get('OIDC_ENABLED') === 'true'; + + if (!oidcEnabled) { + response.status(404).send('OIDC authentication is not enabled'); + return; + } + + // Handles the OIDC callback + 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?error=oidc_failed` + ); + } + } + @Get('webauthn/generate-registration-options') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async generateRegistrationOptions() { diff --git a/apps/api/src/app/auth/auth.module.ts b/apps/api/src/app/auth/auth.module.ts index 824c432b1..33390e394 100644 --- a/apps/api/src/app/auth/auth.module.ts +++ b/apps/api/src/app/auth/auth.module.ts @@ -4,6 +4,7 @@ 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'; @@ -15,6 +16,7 @@ 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 +38,46 @@ import { JwtStrategy } from './jwt.strategy'; AuthService, GoogleStrategy, JwtStrategy, + { + inject: [AuthService, ConfigurationService], + provide: OidcStrategy, + useFactory: async ( + authService: AuthService, + configurationService: ConfigurationService + ) => { + const oidcEnabled = configurationService.get('OIDC_ENABLED') === 'true'; + + if (!oidcEnabled) { + return null; + } + + // Check if we need to fetch discovery config + const authorizationURL = configurationService.get( + 'OIDC_AUTHORIZATION_URL' + ); + const tokenURL = configurationService.get('OIDC_TOKEN_URL'); + const userInfoURL = configurationService.get('OIDC_USER_INFO_URL'); + + if (!authorizationURL || !tokenURL || !userInfoURL) { + // Fetch discovery configuration + try { + const issuer = configurationService.get('OIDC_ISSUER'); + const discovery = await OidcStrategy.fetchDiscoveryConfig(issuer); + + // Temporarily set the discovered URLs in the environment + process.env.OIDC_AUTHORIZATION_URL = + discovery.authorization_endpoint; + process.env.OIDC_TOKEN_URL = discovery.token_endpoint; + process.env.OIDC_USER_INFO_URL = discovery.userinfo_endpoint; + } catch (error) { + console.error('Failed to fetch OIDC discovery:', error); + return null; + } + } + + return new OidcStrategy(authService, configurationService); + } + }, WebAuthService ] }) 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..1ee4bd690 --- /dev/null +++ b/apps/api/src/app/auth/oidc-state.store.ts @@ -0,0 +1,111 @@ +/** + * 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 stateMap = new Map< + string, + { + ctx: { maxAge?: number; nonce?: string; issued?: Date }; + appState?: unknown; + meta?: unknown; + timestamp: number; + } + >(); + private readonly STATE_EXPIRY_MS = 10 * 60 * 1000; // 10 minutes + + /** + * Store request state. + * Signature matches passport-openidconnect SessionStore + */ + public store( + _req: unknown, + ctx: { maxAge?: number; nonce?: string; issued?: Date }, + appState: unknown, + _meta: unknown, + callback: (err: Error | null, handle?: string) => void + ): 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, + ctx?: { maxAge?: number; nonce?: string; issued?: Date }, + appState?: unknown + ) => void + ): void { + try { + const data = this.stateMap.get(handle); + + if (!data) { + return callback(null, undefined, undefined); + } + + // Check if state has expired + if (Date.now() - data.timestamp > this.STATE_EXPIRY_MS) { + 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); + } + } + + /** + * Generate a cryptographically secure random handle + */ + private generateHandle(): string { + return ( + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15) + + Date.now().toString(36) + ); + } + + /** + * Clean up expired states + */ + private cleanup(): void { + 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); + } + } +} 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..835fcb820 --- /dev/null +++ b/apps/api/src/app/auth/oidc.strategy.ts @@ -0,0 +1,123 @@ +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 { + 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 + ) { + // El 'sub' (subject) del ID Token es el identificador único estándar de OIDC + // Según OpenID Connect Core 1.0, el 'sub' es el identificador único e inmutable del usuario + // El 'sub' viene en el idToken parseado o en params + 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 }; + } +} diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index c31f601e3..681e35a3f 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -72,6 +72,24 @@ export class InfoService { if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) { globalPermissions.push(permissions.enableSocialLogin); + + // Determine which social login providers are enabled + const socialLoginProviders: string[] = []; + + const googleClientId = this.configurationService.get('GOOGLE_CLIENT_ID'); + if ( + googleClientId && + googleClientId.trim() !== '' && + googleClientId !== 'dummyClientId' + ) { + socialLoginProviders.push('google'); + } + + if (this.configurationService.get('OIDC_ENABLED') === 'true') { + socialLoginProviders.push('oidc'); + } + + info.socialLoginProviders = socialLoginProviders; } if (this.configurationService.get('ENABLE_FEATURE_STATISTICS')) { diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index 473d909ee..524f4b007 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -55,6 +55,15 @@ export class ConfigurationService { 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: '' }), + OIDC_CLIENT_SECRET: str({ default: '' }), + OIDC_ENABLED: str({ default: 'false' }), + OIDC_ISSUER: str({ default: '' }), + OIDC_SCOPE: str({ default: 'openid profile email' }), + 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 2f94739fb..ffcb81d1b 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -30,6 +30,15 @@ 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_ENABLED: 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 3f011fec4..a04e97911 100644 --- a/apps/client/src/app/components/header/header.component.ts +++ b/apps/client/src/app/components/header/header.component.ts @@ -280,6 +280,7 @@ export class GfHeaderComponent implements OnChanges { data: { accessToken: '', hasPermissionToUseSocialLogin: this.hasPermissionForSocialLogin, + socialLoginProviders: this.info?.socialLoginProviders, title: $localize`Sign in` }, width: '30rem' 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 2fa8b7ea4..446bccad1 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,5 +1,6 @@ export interface LoginWithAccessTokenDialogParams { accessToken: string; hasPermissionToUseSocialLogin: boolean; + socialLoginProviders?: string[]; 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 15e68822a..68654ea5e 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 @@ -25,17 +25,28 @@ @if (data.hasPermissionToUseSocialLogin) {
or
-
- Sign in with Google +
+ @if (data.socialLoginProviders?.includes('google')) { + Sign in with Google + } + @if (data.socialLoginProviders?.includes('oidc')) { + Sign in with OIDC + }
} diff --git a/apps/client/src/app/pages/register/register-page.html b/apps/client/src/app/pages/register/register-page.html index de53777fa..5c0468c42 100644 --- a/apps/client/src/app/pages/register/register-page.html +++ b/apps/client/src/app/pages/register/register-page.html @@ -28,16 +28,29 @@ @if (hasPermissionForSocialLogin) {
or
- Continue with Google +
+ @if (info.socialLoginProviders?.includes('google')) { + Continue with Google + } + @if (info.socialLoginProviders?.includes('oidc')) { + Continue with OIDC + } +
}
diff --git a/libs/common/src/lib/interfaces/info-item.interface.ts b/libs/common/src/lib/interfaces/info-item.interface.ts index fe4101197..8a77d5735 100644 --- a/libs/common/src/lib/interfaces/info-item.interface.ts +++ b/libs/common/src/lib/interfaces/info-item.interface.ts @@ -14,6 +14,7 @@ export interface InfoItem { isDataGatheringEnabled?: string; isReadOnlyMode?: boolean; platforms: Platform[]; + socialLoginProviders?: string[]; statistics: Statistics; stripePublicKey?: string; subscriptionOffer?: SubscriptionOffer; diff --git a/package-lock.json b/package-lock.json index 6429912bb..c2ff93d35 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", @@ -131,6 +132,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", "cypress": "6.2.1", @@ -14475,6 +14477,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", @@ -34517,6 +34543,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 7648cee02..4fe4efff2 100644 --- a/package.json +++ b/package.json @@ -129,6 +129,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", @@ -177,6 +178,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", "cypress": "6.2.1", diff --git a/prisma/migrations/20251103162035_add_oidc_provider/migration.sql b/prisma/migrations/20251103162035_add_oidc_provider/migration.sql new file mode 100644 index 000000000..f71f6eded --- /dev/null +++ b/prisma/migrations/20251103162035_add_oidc_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 {