22 KiB
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.tsyauth.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
- Los usuarios crean una cuenta y reciben un
- 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)
- Usa
- Variables de entorno:
GOOGLE_CLIENT_IDGOOGLE_SECRETROOT_URL
3. WebAuthn (Autenticación sin contraseña)
- ✅ Ubicación:
apps/api/src/app/auth/web-auth.service.ts - Implementación:
- Usa
@simplewebauthn/servery@simplewebauthn/browser - Soporte para dispositivos biométricos (Face ID, Touch ID, etc.)
- Registro y autenticación de dispositivos
- Usa
- Endpoints:
GET /api/auth/webauthn/generate-registration-optionsPOST /api/auth/webauthn/verify-attestationPOST /api/auth/webauthn/generate-authentication-optionsPOST /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-keycon prefijoApi-Key - Para integración con APIs externas
- Usa
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)
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 ✅
- Analizar métodos de autenticación existentes
- Diseñar arquitectura de integración OIDC
- Definir enfoque de coexistencia con métodos actuales
- Decidir estrategia de provider genérico vs específico
Decisiones de diseño:
- Estrategia genérica OIDC configurable para múltiples proveedores
- OIDC coexistirá con los métodos existentes
- Añadir nuevo valor
OIDCal enum Provider - Usar campo
thirdPartyIdexistente para almacenarsubdel token OIDC
Fase 2: Cambios en la Base de Datos ⏳
- Modificar
prisma/schema.prismapara añadir Provider.OIDC - (Opcional) Añadir campo
oidcIssuerpara 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
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
# Crear migración
npx prisma migrate dev --name add_oidc_provider
# O push directo para desarrollo
npm run database:push
Archivos afectados:
prisma/schema.prismaprisma/migrations/YYYYMMDD_add_oidc_provider/migration.sql(nuevo)
Fase 3: Dependencias ⏳
- Instalar
passport-openidconnect - Instalar
@types/passport-openidconnect - Verificar compatibilidad de versiones
- Actualizar
package.jsonypackage-lock.json
Comandos a ejecutar
npm install passport-openidconnect
npm install --save-dev @types/passport-openidconnect
Archivos afectados:
package.jsonpackage-lock.json
Fase 4: Variables de Entorno ⏳
- Actualizar
.env.examplecon 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
# ====================================
# 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
Keycloak
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
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
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
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
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:
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/oidcpara iniciar login - Añadir endpoint
/auth/oidc/callbackpara 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:
@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
validateOAuthLoginfunciona con Provider.OIDC - Añadir lógica específica si es necesario (ej: almacenar issuer)
- Verificar creación de usuarios nuevos
- Verificar que property
isUserSignupEnabledse respeta
Archivo a revisar: apps/api/src/app/auth/auth.service.ts
El método existente ya debería funcionar:
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/:jwtmaneja 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
oidcIssueral modelo User - Modificar
validateOAuthLoginpara 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.mdcon 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:
### 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
```
- Restart the Ghostfolio server
- Users can now authenticate using the "Sign in with OIDC" button
Provider-Specific Setup Guides
See 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