Browse Source

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.
pull/5912/head
Germán Martín 2 months ago
parent
commit
6297535921
  1. 28
      .env.example
  2. 814
      OIDC_IMPLEMENTATION_PLAN.md
  3. 47
      apps/api/src/app/auth/auth.controller.ts
  4. 42
      apps/api/src/app/auth/auth.module.ts
  5. 111
      apps/api/src/app/auth/oidc-state.store.ts
  6. 123
      apps/api/src/app/auth/oidc.strategy.ts
  7. 18
      apps/api/src/app/info/info.service.ts
  8. 9
      apps/api/src/services/configuration/configuration.service.ts
  9. 9
      apps/api/src/services/interfaces/environment.interface.ts
  10. 1
      apps/client/src/app/components/header/header.component.ts
  11. 1
      apps/client/src/app/components/login-with-access-token-dialog/interfaces/interfaces.ts
  12. 13
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html
  13. 13
      apps/client/src/app/pages/register/register-page.html
  14. 1
      libs/common/src/lib/interfaces/info-item.interface.ts
  15. 43
      package-lock.json
  16. 2
      package.json
  17. 2
      prisma/migrations/20251103162035_add_oidc_provider/migration.sql
  18. 1
      prisma/schema.prisma

28
.env.example

@ -14,3 +14,31 @@ POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD>
ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING> ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING>
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
JWT_SECRET_KEY=<INSERT_RANDOM_STRING> JWT_SECRET_KEY=<INSERT_RANDOM_STRING>
ROOT_URL=https://<your_domain>
# 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=<YOUR_CLIENT_ID>
OIDC_CLIENT_SECRET=<YOUR_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

814
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=<YOUR_CLIENT_ID>
OIDC_CLIENT_SECRET=<YOUR_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
<details>
<summary><strong>Keycloak</strong></summary>
```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
```
</details>
<details>
<summary><strong>Auth0</strong></summary>
```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
```
</details>
<details>
<summary><strong>Azure AD</strong></summary>
```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
```
</details>
<details>
<summary><strong>Okta</strong></summary>
```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
```
</details>
**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<string> {
// 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
```

47
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') @Get('webauthn/generate-registration-options')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async generateRegistrationOptions() { public async generateRegistrationOptions() {

42
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 { 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';
@ -15,6 +16,7 @@ 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 +38,46 @@ import { JwtStrategy } from './jwt.strategy';
AuthService, AuthService,
GoogleStrategy, GoogleStrategy,
JwtStrategy, 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 WebAuthService
] ]
}) })

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

123
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<OidcDiscovery> {
const discoveryUrl = `${issuer}.well-known/openid-configuration`;
OidcStrategy.logger.log(
`Fetching OIDC configuration from: ${discoveryUrl}`
);
const response = await fetch(discoveryUrl);
const discovery = (await response.json()) as OidcDiscovery;
OidcStrategy.logger.log(
`OIDC discovery successful. Authorization endpoint: ${discovery.authorization_endpoint}`
);
return discovery;
}
public async validate(
_request: any,
_issuer: string,
profile: any,
context: any,
idToken: any,
_accessToken: any,
_refreshToken: any,
params: any
) {
// 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 };
}
}

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

@ -72,6 +72,24 @@ export class InfoService {
if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) { if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) {
globalPermissions.push(permissions.enableSocialLogin); 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')) { if (this.configurationService.get('ENABLE_FEATURE_STATISTICS')) {

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

@ -55,6 +55,15 @@ export class ConfigurationService {
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: '' }),
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 }), 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

@ -30,6 +30,15 @@ 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_ENABLED: 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;

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

@ -280,6 +280,7 @@ export class GfHeaderComponent implements OnChanges {
data: { data: {
accessToken: '', accessToken: '',
hasPermissionToUseSocialLogin: this.hasPermissionForSocialLogin, hasPermissionToUseSocialLogin: this.hasPermissionForSocialLogin,
socialLoginProviders: this.info?.socialLoginProviders,
title: $localize`Sign in` title: $localize`Sign in`
}, },
width: '30rem' width: '30rem'

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

@ -1,5 +1,6 @@
export interface LoginWithAccessTokenDialogParams { export interface LoginWithAccessTokenDialogParams {
accessToken: string; accessToken: string;
hasPermissionToUseSocialLogin: boolean; hasPermissionToUseSocialLogin: boolean;
socialLoginProviders?: string[];
title: string; title: string;
} }

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

@ -25,7 +25,8 @@
@if (data.hasPermissionToUseSocialLogin) { @if (data.hasPermissionToUseSocialLogin) {
<div class="my-3 text-center text-muted" i18n>or</div> <div class="my-3 text-center text-muted" i18n>or</div>
<div class="d-flex flex-column"> <div class="d-flex flex-column gap-2">
@if (data.socialLoginProviders?.includes('google')) {
<a <a
class="px-4 rounded-pill" class="px-4 rounded-pill"
href="../api/v1/auth/google" href="../api/v1/auth/google"
@ -36,6 +37,16 @@
style="height: 1rem" style="height: 1rem"
/><span i18n>Sign in with Google</span></a /><span i18n>Sign in with Google</span></a
> >
}
@if (data.socialLoginProviders?.includes('oidc')) {
<a
class="px-4 rounded-pill"
href="../api/auth/oidc"
mat-stroked-button
><ion-icon class="mr-2" name="key-outline"></ion-icon
><span i18n>Sign in with OIDC</span></a
>
}
</div> </div>
} }
</form> </form>

13
apps/client/src/app/pages/register/register-page.html

@ -28,6 +28,8 @@
</button> </button>
@if (hasPermissionForSocialLogin) { @if (hasPermissionForSocialLogin) {
<div class="my-3 text-muted" i18n>or</div> <div class="my-3 text-muted" i18n>or</div>
<div class="d-flex flex-column gap-2">
@if (info.socialLoginProviders?.includes('google')) {
<a <a
class="px-4 rounded-pill w-100" class="px-4 rounded-pill w-100"
href="../api/v1/auth/google" href="../api/v1/auth/google"
@ -39,6 +41,17 @@
/><span i18n>Continue with Google</span></a /><span i18n>Continue with Google</span></a
> >
} }
@if (info.socialLoginProviders?.includes('oidc')) {
<a
class="px-4 rounded-pill w-100"
href="../api/auth/oidc"
mat-stroked-button
><ion-icon class="mr-2" name="key-outline"></ion-icon
><span i18n>Continue with OIDC</span></a
>
}
</div>
}
</div> </div>
</div> </div>
</div> </div>

1
libs/common/src/lib/interfaces/info-item.interface.ts

@ -14,6 +14,7 @@ export interface InfoItem {
isDataGatheringEnabled?: string; isDataGatheringEnabled?: string;
isReadOnlyMode?: boolean; isReadOnlyMode?: boolean;
platforms: Platform[]; platforms: Platform[];
socialLoginProviders?: string[];
statistics: Statistics; statistics: Statistics;
stripePublicKey?: string; stripePublicKey?: string;
subscriptionOffer?: SubscriptionOffer; subscriptionOffer?: SubscriptionOffer;

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",
@ -131,6 +132,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",
"cypress": "6.2.1", "cypress": "6.2.1",
@ -14475,6 +14477,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",
@ -34517,6 +34543,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

@ -129,6 +129,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",
@ -177,6 +178,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",
"cypress": "6.2.1", "cypress": "6.2.1",

2
prisma/migrations/20251103162035_add_oidc_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