changes: Handle validation external auth flow
This commit is contained in:
219
EXTERNAL_AUTH_BEST_PRACTICES.md
Normal file
219
EXTERNAL_AUTH_BEST_PRACTICES.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# External Authentication - Best Practices Implementation
|
||||
|
||||
Este documento describe las mejores prácticas implementadas en el sistema de autenticación externa (SSO).
|
||||
|
||||
## ✅ Mejoras Implementadas
|
||||
|
||||
### 1. **Seguridad**
|
||||
|
||||
#### Validación de Tokens Robusta
|
||||
- ✅ Validación de firma con JWKS
|
||||
- ✅ Validación de issuer, audience, y lifetime
|
||||
- ✅ Clock skew configurable para tolerancia de tiempo
|
||||
- ✅ Manejo específico de excepciones de seguridad
|
||||
- ✅ Logging detallado de errores de validación sin exponer información sensible
|
||||
|
||||
#### Protección de Datos Sensibles
|
||||
- ✅ Claims sensibles redactados en logs de producción
|
||||
- ✅ Logs de debug solo habilitados con configuración explícita
|
||||
- ✅ Secrets encriptados en base de datos
|
||||
|
||||
#### Validación de Entrada
|
||||
- ✅ Validación de modelo en controller
|
||||
- ✅ Verificación de campos requeridos antes de procesar
|
||||
|
||||
### 2. **Arquitectura y Código Limpio**
|
||||
|
||||
#### Separation of Concerns
|
||||
- ✅ `TokenAuthController`: Solo manejo de HTTP y orquestación
|
||||
- ✅ `ExternalAuthenticationManager`: Lógica de negocio de autenticación
|
||||
- ✅ `ExternalAuthProviderManager`: Gestión de configuración de providers
|
||||
|
||||
#### DRY (Don't Repeat Yourself)
|
||||
- ✅ Método `CreateClaimsIdentityFromUser` reutilizable para local y external auth
|
||||
- ✅ `GetClaimValue` centraliza lógica de mapeo de claims
|
||||
- ✅ Constantes en `ExternalAuthConstants` evitan magic strings
|
||||
|
||||
#### Código Mantenible
|
||||
- ✅ Nombres descriptivos y consistentes
|
||||
- ✅ Documentación XML en métodos públicos
|
||||
- ✅ Constantes agrupadas lógicamente
|
||||
- ✅ Mapeo de claims con diccionario extensible
|
||||
|
||||
### 3. **Configurabilidad**
|
||||
|
||||
#### Settings Flexibles
|
||||
```
|
||||
App.ExternalAuth.Enabled
|
||||
App.ExternalAuth.AutoProvisionUsers
|
||||
App.ExternalAuth.DefaultRole
|
||||
App.ExternalAuth.UpdateUserInfoOnLogin
|
||||
App.ExternalAuth.JwksCacheDurationMinutes
|
||||
App.ExternalAuth.ClockSkewMinutes
|
||||
App.ExternalAuth.EnableDebugLogging
|
||||
```
|
||||
|
||||
#### Valores por Defecto Razonables
|
||||
- JWKS cache: 60 minutos
|
||||
- Discovery cache: 60 minutos
|
||||
- Clock skew: 5 minutos
|
||||
- Token validation timeout: 10 segundos
|
||||
|
||||
### 4. **Performance** (Preparado para implementar)
|
||||
|
||||
#### Cache Strategy (TODO)
|
||||
```csharp
|
||||
// Cache de JWKS para evitar HTTP requests en cada validación
|
||||
// Cache de OIDC discovery document
|
||||
// TTL configurable por provider
|
||||
```
|
||||
|
||||
### 5. **Manejo de Errores**
|
||||
|
||||
#### Excepciones Específicas
|
||||
- ✅ `SecurityTokenExpiredException` → "Token has expired"
|
||||
- ✅ `SecurityTokenInvalidSignatureException` → "Token signature is invalid"
|
||||
- ✅ `SecurityTokenInvalidIssuerException` → "Token issuer mismatch"
|
||||
- ✅ `SecurityTokenInvalidAudienceException` → "Token audience mismatch"
|
||||
- ✅ `HttpRequestException` → "Unable to connect to provider"
|
||||
- ✅ `JsonException` → "Invalid response from provider"
|
||||
|
||||
#### User-Friendly Messages
|
||||
- Mensajes claros para el usuario
|
||||
- Detalles técnicos solo en logs del servidor
|
||||
- No exponer información sensible de configuración
|
||||
|
||||
### 6. **Extensibilidad**
|
||||
|
||||
#### Soporte Multi-Provider
|
||||
- Sistema diseñado para múltiples providers simultáneos
|
||||
- Configuración por provider en base de datos
|
||||
- Mapeo de claims configurable por provider
|
||||
|
||||
#### Claim Mapping Flexible
|
||||
```csharp
|
||||
// Standard OIDC claims
|
||||
ExternalAuthConstants.OidcClaims.*
|
||||
|
||||
// Alternative claims (diferentes providers)
|
||||
ExternalAuthConstants.AlternativeClaims.*
|
||||
|
||||
// Mapeo a Microsoft ClaimTypes
|
||||
StandardToMicrosoftClaimTypeMap
|
||||
```
|
||||
|
||||
### 7. **Auditoría y Compliance**
|
||||
|
||||
#### Audit Trail
|
||||
- ✅ `UserLoginAttempt` registra cada intento
|
||||
- ✅ `UserLogin` vincula usuario con provider
|
||||
- ✅ Información de provider y timestamp
|
||||
|
||||
#### Logging Estratégico
|
||||
- Error logs: Problemas de seguridad y configuración
|
||||
- Warn logs: Tokens expirados, keys no encontradas
|
||||
- Debug logs: Claims y flujo detallado (solo desarrollo)
|
||||
|
||||
## 🔄 Flujo de Autenticación Externa
|
||||
|
||||
```
|
||||
1. Frontend obtiene ID token de Authentik/Keycloak/etc
|
||||
↓
|
||||
2. POST /api/TokenAuth/AuthenticateExternal
|
||||
- Validación de entrada
|
||||
↓
|
||||
3. ExternalAuthenticationManager.AuthenticateWithExternalTokenAsync
|
||||
- Verificar que external auth esté habilitado
|
||||
- Obtener configuración del provider
|
||||
↓
|
||||
4. ValidateTokenAsync
|
||||
- Descargar OIDC discovery (.well-known/openid-configuration)
|
||||
- Descargar JWKS (JSON Web Key Set)
|
||||
- Validar firma, issuer, audience, lifetime
|
||||
- Extraer claims
|
||||
↓
|
||||
5. FindOrCreateUserAsync
|
||||
- Buscar UserLogin existente (provider + sub)
|
||||
- Si no existe y auto-provision habilitado:
|
||||
* Crear usuario con email del token
|
||||
* Vincular con provider en UserLogin table
|
||||
↓
|
||||
6. CreateClaimsIdentityFromUser
|
||||
- Construir ClaimsIdentity con datos del usuario local
|
||||
↓
|
||||
7. CreateAccessToken
|
||||
- Generar JWT del backend (NO passthrough)
|
||||
- Firmar con SecurityKey del backend
|
||||
- Expiración configurable
|
||||
↓
|
||||
8. Retornar token del backend al frontend
|
||||
- Frontend usa este token para llamadas subsecuentes
|
||||
- Backend puede validar con su propia clave
|
||||
```
|
||||
|
||||
## 🎯 Próximas Mejoras Sugeridas
|
||||
|
||||
### Alta Prioridad
|
||||
1. **Implementar cache de JWKS y Discovery**
|
||||
- Usar `ICacheManager` de ABP
|
||||
- TTL configurable
|
||||
- Invalidación manual por proveedor
|
||||
|
||||
2. **Rate Limiting en endpoint público**
|
||||
- Prevenir ataques de fuerza bruta
|
||||
- Configuración por IP o por provider
|
||||
|
||||
3. **Actualización de usuario en cada login**
|
||||
- Implementar `UpdateUserInfoOnLogin` setting
|
||||
- Sync de email, nombre desde claims del provider
|
||||
|
||||
### Media Prioridad
|
||||
4. **Role Mapping desde Claims**
|
||||
- Mapear claims de grupos/roles del provider
|
||||
- Asignar roles de ABP automáticamente
|
||||
|
||||
5. **Refresh Token Support**
|
||||
- Almacenar refresh tokens
|
||||
- Auto-renovación de sesión
|
||||
|
||||
6. **Tests Unitarios**
|
||||
- Tests para `GetClaimValue`
|
||||
- Tests para validación de tokens (con tokens mock)
|
||||
- Tests para JIT provisioning
|
||||
|
||||
### Baja Prioridad
|
||||
7. **Métricas y Monitoreo**
|
||||
- Contador de logins por provider
|
||||
- Tiempo de respuesta de validación
|
||||
- Fallos por tipo de error
|
||||
|
||||
8. **Multi-Factor Authentication (MFA)**
|
||||
- Validar `acr` claim
|
||||
- Requerir MFA para ciertos roles
|
||||
|
||||
## 📚 Referencias
|
||||
|
||||
- [OIDC Core Specification](https://openid.net/specs/openid-connect-core-1_0.html)
|
||||
- [JWT Best Practices](https://tools.ietf.org/html/rfc8725)
|
||||
- [ASP.NET Core Security](https://learn.microsoft.com/en-us/aspnet/core/security/)
|
||||
- [ABP Framework Authorization](https://docs.abp.io/en/abp/latest/Authorization)
|
||||
|
||||
## 🔐 Consideraciones de Seguridad
|
||||
|
||||
### Producción
|
||||
- ⚠️ SIEMPRE usar HTTPS
|
||||
- ⚠️ `EnableDebugLogging = false`
|
||||
- ⚠️ Rotar `SecurityKey` regularmente
|
||||
- ⚠️ Configurar `RequireHttpsMetadata = true` en providers
|
||||
- ⚠️ Monitorear logs de intentos fallidos
|
||||
|
||||
### Desarrollo
|
||||
- ✅ Usar providers de test separados
|
||||
- ✅ No exponer client secrets en repositorio
|
||||
- ✅ Usar variables de entorno o secret managers
|
||||
|
||||
---
|
||||
|
||||
**Última actualización**: 2025-10-15
|
||||
**Versión**: 1.0
|
||||
**Implementado por**: JJSolutions - SSO Implementation Team
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace ASPBaseOIDC.Authorization.ExternalAuth;
|
||||
|
||||
/// <summary>
|
||||
/// Constants for external authentication
|
||||
/// </summary>
|
||||
public static class ExternalAuthConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// Standard OIDC claim types
|
||||
/// </summary>
|
||||
public static class OidcClaims
|
||||
{
|
||||
public const string Sub = "sub";
|
||||
public const string Email = "email";
|
||||
public const string EmailVerified = "email_verified";
|
||||
public const string Name = "name";
|
||||
public const string GivenName = "given_name";
|
||||
public const string FamilyName = "family_name";
|
||||
public const string PreferredUsername = "preferred_username";
|
||||
public const string Nickname = "nickname";
|
||||
public const string Picture = "picture";
|
||||
public const string Locale = "locale";
|
||||
public const string UpdatedAt = "updated_at";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Alternative claim types used by different providers
|
||||
/// </summary>
|
||||
public static class AlternativeClaims
|
||||
{
|
||||
public const string Uid = "uid";
|
||||
public const string UserId = "user_id";
|
||||
public const string Username = "username";
|
||||
public const string UserPrincipalName = "upn";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cache keys for external auth
|
||||
/// </summary>
|
||||
public static class CacheKeys
|
||||
{
|
||||
public const string JwksPrefix = "ExternalAuth:Jwks:";
|
||||
public const string OidcDiscoveryPrefix = "ExternalAuth:Discovery:";
|
||||
public const string EnabledProviders = "ExternalAuth:EnabledProviders";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default configuration values
|
||||
/// </summary>
|
||||
public static class Defaults
|
||||
{
|
||||
public const int JwksCacheDurationMinutes = 60;
|
||||
public const int DiscoveryCacheDurationMinutes = 60;
|
||||
public const int TokenValidationTimeoutSeconds = 10;
|
||||
public const int ClockSkewMinutes = 5;
|
||||
public const int MaxLoginAttemptAgeMinutes = 15;
|
||||
}
|
||||
}
|
||||
@@ -83,23 +83,29 @@ public class ExternalAuthenticationManager : DomainService
|
||||
// 3. Validate JWT token
|
||||
var claims = await ValidateTokenAsync(provider, idToken);
|
||||
|
||||
// Log all claims for debugging
|
||||
Logger.Info("Claims received from token:");
|
||||
foreach (var claim in claims)
|
||||
// Log all claims for debugging (only in Debug mode)
|
||||
if (Logger.IsDebugEnabled)
|
||||
{
|
||||
Logger.Info($" {claim.Type} = {claim.Value}");
|
||||
Logger.Debug($"Claims received from {provider.Name} token:");
|
||||
foreach (var claim in claims)
|
||||
{
|
||||
// Don't log sensitive values in production
|
||||
var value = claim.Type.Contains("token", StringComparison.OrdinalIgnoreCase)
|
||||
? "***REDACTED***"
|
||||
: claim.Value;
|
||||
Logger.Debug($" {claim.Type} = {value}");
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find subject identifier (sub, uid, user_id, etc.)
|
||||
var sub = claims.FirstOrDefault(c => c.Type == "sub")?.Value
|
||||
?? claims.FirstOrDefault(c => c.Type == "uid")?.Value
|
||||
?? claims.FirstOrDefault(c => c.Type == "user_id")?.Value
|
||||
// Try to find subject identifier using constants
|
||||
var sub = claims.FirstOrDefault(c => c.Type == ExternalAuthConstants.OidcClaims.Sub)?.Value
|
||||
?? claims.FirstOrDefault(c => c.Type == ExternalAuthConstants.AlternativeClaims.Uid)?.Value
|
||||
?? claims.FirstOrDefault(c => c.Type == ExternalAuthConstants.AlternativeClaims.UserId)?.Value
|
||||
?? claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
// Try to find email using multiple formats
|
||||
var email = claims.FirstOrDefault(c => c.Type == "email")?.Value
|
||||
?? claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value
|
||||
?? claims.FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")?.Value;
|
||||
var email = claims.FirstOrDefault(c => c.Type == ExternalAuthConstants.OidcClaims.Email)?.Value
|
||||
?? claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(sub))
|
||||
{
|
||||
@@ -152,6 +158,22 @@ public class ExternalAuthenticationManager : DomainService
|
||||
// Download JWKS
|
||||
var jwksResponse = await httpClient.GetStringAsync(jwksUri);
|
||||
|
||||
// Get configurable clock skew (default: 5 minutes)
|
||||
var clockSkewMinutes = ExternalAuthConstants.Defaults.ClockSkewMinutes;
|
||||
try
|
||||
{
|
||||
var clockSkewSetting = await _settingManager.GetSettingValueForApplicationAsync(
|
||||
AppSettingNames.ExternalAuth.ClockSkewMinutes);
|
||||
if (int.TryParse(clockSkewSetting, out var parsedValue) && parsedValue > 0)
|
||||
{
|
||||
clockSkewMinutes = parsedValue;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Setting doesn't exist, use default
|
||||
}
|
||||
|
||||
// Create validation parameters
|
||||
var validationParameters = new TokenValidationParameters
|
||||
{
|
||||
@@ -163,40 +185,100 @@ public class ExternalAuthenticationManager : DomainService
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKeyResolver = (token, securityToken, kid, parameters) =>
|
||||
{
|
||||
var jwks = JsonDocument.Parse(jwksResponse);
|
||||
var keys = jwks.RootElement.GetProperty("keys");
|
||||
var signingKeys = new List<SecurityKey>();
|
||||
|
||||
foreach (var key in keys.EnumerateArray())
|
||||
try
|
||||
{
|
||||
var keyId = key.GetProperty("kid").GetString();
|
||||
if (keyId == kid)
|
||||
var jwks = JsonDocument.Parse(jwksResponse);
|
||||
if (!jwks.RootElement.TryGetProperty("keys", out var keysElement))
|
||||
{
|
||||
var e = key.GetProperty("e").GetString();
|
||||
var n = key.GetProperty("n").GetString();
|
||||
|
||||
var rsa = new RsaSecurityKey(new System.Security.Cryptography.RSAParameters
|
||||
{
|
||||
Exponent = Base64UrlEncoder.DecodeBytes(e),
|
||||
Modulus = Base64UrlEncoder.DecodeBytes(n)
|
||||
});
|
||||
signingKeys.Add(rsa);
|
||||
Logger.Error("Invalid JWKS response: 'keys' property not found");
|
||||
return Array.Empty<SecurityKey>();
|
||||
}
|
||||
}
|
||||
|
||||
return signingKeys;
|
||||
var signingKeys = new List<SecurityKey>();
|
||||
|
||||
foreach (var key in keysElement.EnumerateArray())
|
||||
{
|
||||
if (!key.TryGetProperty("kid", out var kidProperty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var keyId = kidProperty.GetString();
|
||||
if (keyId == kid)
|
||||
{
|
||||
if (key.TryGetProperty("kty", out var kty) && kty.GetString() == "RSA")
|
||||
{
|
||||
var e = key.GetProperty("e").GetString();
|
||||
var n = key.GetProperty("n").GetString();
|
||||
|
||||
var rsa = new RsaSecurityKey(new System.Security.Cryptography.RSAParameters
|
||||
{
|
||||
Exponent = Base64UrlEncoder.DecodeBytes(e),
|
||||
Modulus = Base64UrlEncoder.DecodeBytes(n)
|
||||
})
|
||||
{
|
||||
KeyId = keyId
|
||||
};
|
||||
|
||||
signingKeys.Add(rsa);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (signingKeys.Count == 0)
|
||||
{
|
||||
Logger.Warn($"No matching signing key found for kid: {kid}");
|
||||
}
|
||||
|
||||
return signingKeys;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Error parsing JWKS: {ex.Message}", ex);
|
||||
return Array.Empty<SecurityKey>();
|
||||
}
|
||||
},
|
||||
ClockSkew = TimeSpan.FromMinutes(5)
|
||||
ClockSkew = TimeSpan.FromMinutes(clockSkewMinutes)
|
||||
};
|
||||
|
||||
// Validate token
|
||||
var principal = handler.ValidateToken(idToken, validationParameters, out var validatedToken);
|
||||
return principal.Claims.ToList();
|
||||
}
|
||||
catch (SecurityTokenExpiredException ex)
|
||||
{
|
||||
Logger.Warn($"Token expired for provider {provider.Name}: {ex.Message}");
|
||||
throw new UserFriendlyException("Token has expired. Please sign in again.");
|
||||
}
|
||||
catch (SecurityTokenInvalidSignatureException ex)
|
||||
{
|
||||
Logger.Error($"Token signature validation failed for provider {provider.Name}: {ex.Message}", ex);
|
||||
throw new UserFriendlyException("Token signature is invalid. This may indicate a security issue.");
|
||||
}
|
||||
catch (SecurityTokenInvalidIssuerException ex)
|
||||
{
|
||||
Logger.Error($"Token issuer validation failed for provider {provider.Name}. Expected: {provider.Authority}, Got: {ex.InvalidIssuer}", ex);
|
||||
throw new UserFriendlyException($"Token issuer mismatch. Please check provider configuration.");
|
||||
}
|
||||
catch (SecurityTokenInvalidAudienceException ex)
|
||||
{
|
||||
Logger.Error($"Token audience validation failed for provider {provider.Name}. Expected: {provider.ClientId}", ex);
|
||||
throw new UserFriendlyException("Token audience mismatch. Please check provider configuration.");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Logger.Error($"Failed to reach OIDC endpoints for provider {provider.Name}: {ex.Message}", ex);
|
||||
throw new UserFriendlyException("Unable to connect to authentication provider. Please try again later.");
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Logger.Error($"Failed to parse OIDC response for provider {provider.Name}: {ex.Message}", ex);
|
||||
throw new UserFriendlyException("Invalid response from authentication provider.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Token validation failed", ex);
|
||||
throw new UserFriendlyException("Invalid token: " + ex.Message);
|
||||
Logger.Error($"Unexpected error during token validation for provider {provider.Name}: {ex.Message}", ex);
|
||||
throw new UserFriendlyException("An error occurred during authentication. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,30 +429,55 @@ public class ExternalAuthenticationManager : DomainService
|
||||
await CurrentUnitOfWork.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get claim value with fallback to standard Microsoft claim types
|
||||
/// </summary>
|
||||
private string GetClaimValue(List<Claim> claims, Dictionary<string, string> mappings, string claimType)
|
||||
{
|
||||
if (!mappings.TryGetValue(claimType, out var mappedType))
|
||||
// Try custom mapping first
|
||||
if (mappings.TryGetValue(claimType, out var mappedType))
|
||||
{
|
||||
mappedType = claimType;
|
||||
var value = claims.FirstOrDefault(c => c.Type == mappedType)?.Value;
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find the claim by mapped type or standard type
|
||||
var value = claims.FirstOrDefault(c => c.Type == mappedType)?.Value;
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
// Try standard OIDC claim type
|
||||
var standardValue = claims.FirstOrDefault(c => c.Type == claimType)?.Value;
|
||||
if (!string.IsNullOrEmpty(standardValue))
|
||||
{
|
||||
return value;
|
||||
return standardValue;
|
||||
}
|
||||
|
||||
// Fallback: Try common Microsoft claim type mappings
|
||||
return claimType switch
|
||||
// Fallback: Try Microsoft .NET claim type equivalents
|
||||
return GetMicrosoftClaimTypeFallback(claims, claimType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps OIDC standard claims to Microsoft .NET ClaimTypes
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> StandardToMicrosoftClaimTypeMap = new()
|
||||
{
|
||||
{ ExternalAuthConstants.OidcClaims.Email, ClaimTypes.Email },
|
||||
{ ExternalAuthConstants.OidcClaims.Name, ClaimTypes.Name },
|
||||
{ ExternalAuthConstants.OidcClaims.GivenName, ClaimTypes.GivenName },
|
||||
{ ExternalAuthConstants.OidcClaims.FamilyName, ClaimTypes.Surname },
|
||||
{ ExternalAuthConstants.OidcClaims.PreferredUsername, ClaimTypes.Name },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Fallback to Microsoft claim types when standard OIDC claims not found
|
||||
/// </summary>
|
||||
private string GetMicrosoftClaimTypeFallback(List<Claim> claims, string claimType)
|
||||
{
|
||||
if (StandardToMicrosoftClaimTypeMap.TryGetValue(claimType, out var microsoftType))
|
||||
{
|
||||
"email" => claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value,
|
||||
"name" => claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value,
|
||||
"given_name" => claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value,
|
||||
"family_name" => claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value,
|
||||
"preferred_username" => claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value,
|
||||
_ => null
|
||||
};
|
||||
return claims.FirstOrDefault(c => c.Type == microsoftType)?.Value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private int GetTokenExpiration(string idToken)
|
||||
|
||||
@@ -28,6 +28,37 @@ public static class AppSettingNames
|
||||
/// Default role name for auto-provisioned users (empty = no role assigned)
|
||||
/// </summary>
|
||||
public const string DefaultRole = "App.ExternalAuth.DefaultRole";
|
||||
|
||||
/// <summary>
|
||||
/// Update user information from external provider on each login
|
||||
/// </summary>
|
||||
public const string UpdateUserInfoOnLogin = "App.ExternalAuth.UpdateUserInfoOnLogin";
|
||||
|
||||
/// <summary>
|
||||
/// Cache duration for JWKS keys in minutes (default: 60)
|
||||
/// </summary>
|
||||
public const string JwksCacheDurationMinutes = "App.ExternalAuth.JwksCacheDurationMinutes";
|
||||
|
||||
/// <summary>
|
||||
/// Cache duration for OIDC discovery document in minutes (default: 60)
|
||||
/// </summary>
|
||||
public const string DiscoveryCacheDurationMinutes = "App.ExternalAuth.DiscoveryCacheDurationMinutes";
|
||||
|
||||
/// <summary>
|
||||
/// Token validation timeout in seconds (default: 10)
|
||||
/// </summary>
|
||||
public const string TokenValidationTimeoutSeconds = "App.ExternalAuth.TokenValidationTimeoutSeconds";
|
||||
|
||||
/// <summary>
|
||||
/// Clock skew tolerance for token validation in minutes (default: 5)
|
||||
/// </summary>
|
||||
public const string ClockSkewMinutes = "App.ExternalAuth.ClockSkewMinutes";
|
||||
|
||||
/// <summary>
|
||||
/// Enable debug logging for external authentication (default: false)
|
||||
/// WARNING: May log sensitive information, only enable in development
|
||||
/// </summary>
|
||||
public const string EnableDebugLogging = "App.ExternalAuth.EnableDebugLogging";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Abp.Authorization.Users;
|
||||
using Abp.MultiTenancy;
|
||||
using Abp.Runtime.Security;
|
||||
using Abp.UI;
|
||||
using ASPBaseOIDC.Application.Authorization.ExternalAuth.Dto;
|
||||
using ASPBaseOIDC.Authentication.JwtBearer;
|
||||
using ASPBaseOIDC.Authorization;
|
||||
@@ -70,6 +71,17 @@ namespace ASPBaseOIDC.Controllers
|
||||
[AbpAllowAnonymous]
|
||||
public async Task<AuthenticateResultModel> AuthenticateExternal([FromBody] ExternalAuthModel model)
|
||||
{
|
||||
// Validate input
|
||||
if (string.IsNullOrWhiteSpace(model?.ProviderName))
|
||||
{
|
||||
throw new UserFriendlyException("Provider name is required");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(model.IdToken))
|
||||
{
|
||||
throw new UserFriendlyException("ID token is required");
|
||||
}
|
||||
|
||||
// Authenticate with external provider (validates token, provisions user if needed)
|
||||
var result = await _externalAuthManager.AuthenticateWithExternalTokenAsync(
|
||||
model.ProviderName,
|
||||
@@ -77,24 +89,15 @@ namespace ASPBaseOIDC.Controllers
|
||||
AbpSession.TenantId
|
||||
);
|
||||
|
||||
// Create claims identity for the user (same as local login)
|
||||
var identity = new ClaimsIdentity();
|
||||
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, result.User.Id.ToString()));
|
||||
identity.AddClaim(new Claim(ClaimTypes.Name, result.User.UserName));
|
||||
identity.AddClaim(new Claim(ClaimTypes.Email, result.User.EmailAddress));
|
||||
|
||||
// Add tenant claim if applicable
|
||||
if (result.User.TenantId.HasValue)
|
||||
{
|
||||
identity.AddClaim(new Claim(AbpClaimTypes.TenantId, result.User.TenantId.Value.ToString()));
|
||||
}
|
||||
// Create claims identity for the authenticated user
|
||||
var identity = CreateClaimsIdentityFromUser(result.User);
|
||||
|
||||
// Generate backend's own JWT token
|
||||
var accessToken = CreateAccessToken(CreateJwtClaims(identity));
|
||||
|
||||
return new AuthenticateResultModel
|
||||
{
|
||||
AccessToken = accessToken, // Backend's own JWT
|
||||
AccessToken = accessToken,
|
||||
EncryptedAccessToken = GetEncryptedAccessToken(accessToken),
|
||||
ExpireInSeconds = (int)_configuration.Expiration.TotalSeconds,
|
||||
UserId = result.User.Id
|
||||
@@ -160,6 +163,45 @@ namespace ASPBaseOIDC.Controllers
|
||||
{
|
||||
return SimpleStringCipher.Instance.Encrypt(accessToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ClaimsIdentity from a User entity
|
||||
/// Centralizes claim creation logic for both local and external authentication
|
||||
/// </summary>
|
||||
private ClaimsIdentity CreateClaimsIdentityFromUser(User user)
|
||||
{
|
||||
var identity = new ClaimsIdentity();
|
||||
|
||||
// Standard claims
|
||||
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
|
||||
identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName));
|
||||
|
||||
if (!string.IsNullOrEmpty(user.EmailAddress))
|
||||
{
|
||||
identity.AddClaim(new Claim(ClaimTypes.Email, user.EmailAddress));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(user.Name))
|
||||
{
|
||||
identity.AddClaim(new Claim(ClaimTypes.GivenName, user.Name));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(user.Surname))
|
||||
{
|
||||
identity.AddClaim(new Claim(ClaimTypes.Surname, user.Surname));
|
||||
}
|
||||
|
||||
// Tenant claim
|
||||
if (user.TenantId.HasValue)
|
||||
{
|
||||
identity.AddClaim(new Claim(AbpClaimTypes.TenantId, user.TenantId.Value.ToString()));
|
||||
}
|
||||
|
||||
// Note: Roles and permissions will be added by ABP authorization system
|
||||
// when user makes authorized requests
|
||||
|
||||
return identity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user