diff --git a/EXTERNAL_AUTH_BEST_PRACTICES.md b/EXTERNAL_AUTH_BEST_PRACTICES.md
new file mode 100644
index 0000000..dbff1f0
--- /dev/null
+++ b/EXTERNAL_AUTH_BEST_PRACTICES.md
@@ -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
diff --git a/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthConstants.cs b/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthConstants.cs
new file mode 100644
index 0000000..6f0b7dd
--- /dev/null
+++ b/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthConstants.cs
@@ -0,0 +1,58 @@
+namespace ASPBaseOIDC.Authorization.ExternalAuth;
+
+///
+/// Constants for external authentication
+///
+public static class ExternalAuthConstants
+{
+ ///
+ /// Standard OIDC claim types
+ ///
+ 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";
+ }
+
+ ///
+ /// Alternative claim types used by different providers
+ ///
+ 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";
+ }
+
+ ///
+ /// Cache keys for external auth
+ ///
+ public static class CacheKeys
+ {
+ public const string JwksPrefix = "ExternalAuth:Jwks:";
+ public const string OidcDiscoveryPrefix = "ExternalAuth:Discovery:";
+ public const string EnabledProviders = "ExternalAuth:EnabledProviders";
+ }
+
+ ///
+ /// Default configuration values
+ ///
+ 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;
+ }
+}
diff --git a/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthenticationManager.cs b/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthenticationManager.cs
index 90e381b..d89f796 100644
--- a/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthenticationManager.cs
+++ b/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthenticationManager.cs
@@ -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();
-
- 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();
}
- }
- return signingKeys;
+ var signingKeys = new List();
+
+ 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();
+ }
},
- 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();
}
+ ///
+ /// Get claim value with fallback to standard Microsoft claim types
+ ///
private string GetClaimValue(List claims, Dictionary 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);
+ }
+
+ ///
+ /// Maps OIDC standard claims to Microsoft .NET ClaimTypes
+ ///
+ private static readonly Dictionary 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 },
+ };
+
+ ///
+ /// Fallback to Microsoft claim types when standard OIDC claims not found
+ ///
+ private string GetMicrosoftClaimTypeFallback(List 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)
diff --git a/src/ASPBaseOIDC.Core/Configuration/AppSettingNames.cs b/src/ASPBaseOIDC.Core/Configuration/AppSettingNames.cs
index 592ab29..8679283 100644
--- a/src/ASPBaseOIDC.Core/Configuration/AppSettingNames.cs
+++ b/src/ASPBaseOIDC.Core/Configuration/AppSettingNames.cs
@@ -28,6 +28,37 @@ public static class AppSettingNames
/// Default role name for auto-provisioned users (empty = no role assigned)
///
public const string DefaultRole = "App.ExternalAuth.DefaultRole";
+
+ ///
+ /// Update user information from external provider on each login
+ ///
+ public const string UpdateUserInfoOnLogin = "App.ExternalAuth.UpdateUserInfoOnLogin";
+
+ ///
+ /// Cache duration for JWKS keys in minutes (default: 60)
+ ///
+ public const string JwksCacheDurationMinutes = "App.ExternalAuth.JwksCacheDurationMinutes";
+
+ ///
+ /// Cache duration for OIDC discovery document in minutes (default: 60)
+ ///
+ public const string DiscoveryCacheDurationMinutes = "App.ExternalAuth.DiscoveryCacheDurationMinutes";
+
+ ///
+ /// Token validation timeout in seconds (default: 10)
+ ///
+ public const string TokenValidationTimeoutSeconds = "App.ExternalAuth.TokenValidationTimeoutSeconds";
+
+ ///
+ /// Clock skew tolerance for token validation in minutes (default: 5)
+ ///
+ public const string ClockSkewMinutes = "App.ExternalAuth.ClockSkewMinutes";
+
+ ///
+ /// Enable debug logging for external authentication (default: false)
+ /// WARNING: May log sensitive information, only enable in development
+ ///
+ public const string EnableDebugLogging = "App.ExternalAuth.EnableDebugLogging";
}
}
diff --git a/src/ASPBaseOIDC.Web.Core/Controllers/TokenAuthController.cs b/src/ASPBaseOIDC.Web.Core/Controllers/TokenAuthController.cs
index a3c6f1b..91c6963 100644
--- a/src/ASPBaseOIDC.Web.Core/Controllers/TokenAuthController.cs
+++ b/src/ASPBaseOIDC.Web.Core/Controllers/TokenAuthController.cs
@@ -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 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);
}
+
+ ///
+ /// Creates a ClaimsIdentity from a User entity
+ /// Centralizes claim creation logic for both local and external authentication
+ ///
+ 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;
+ }
}
}