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; + } } }