diff --git a/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/Dto/CreateOrUpdateProviderInput.cs b/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/Dto/CreateOrUpdateProviderInput.cs new file mode 100644 index 0000000..bb843ae --- /dev/null +++ b/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/Dto/CreateOrUpdateProviderInput.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; + +namespace ASPBaseOIDC.Application.Authorization.ExternalAuth.Dto; + +public class CreateOrUpdateProviderInput +{ + public int? Id { get; set; } + + [Required] + [MaxLength(128)] + public string Name { get; set; } + + [Required] + [MaxLength(64)] + public string ProviderType { get; set; } + + public bool IsEnabled { get; set; } + + [Required] + [MaxLength(512)] + public string Authority { get; set; } + + [Required] + [MaxLength(256)] + public string ClientId { get; set; } + + [Required] + [MaxLength(512)] + public string ClientSecret { get; set; } + + [MaxLength(512)] + public string Scopes { get; set; } + + [MaxLength(64)] + public string ResponseType { get; set; } + + public bool RequireHttpsMetadata { get; set; } + + [MaxLength(2048)] + public string ClaimMappings { get; set; } + + public int DisplayOrder { get; set; } +} diff --git a/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/Dto/ExternalAuthModel.cs b/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/Dto/ExternalAuthModel.cs new file mode 100644 index 0000000..623bb3f --- /dev/null +++ b/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/Dto/ExternalAuthModel.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace ASPBaseOIDC.Application.Authorization.ExternalAuth.Dto; + +/// +/// Model for external authentication request +/// +public class ExternalAuthModel +{ + [Required] + public string ProviderName { get; set; } + + [Required] + public string IdToken { get; set; } +} diff --git a/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/Dto/ExternalAuthProviderDto.cs b/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/Dto/ExternalAuthProviderDto.cs new file mode 100644 index 0000000..689eaf1 --- /dev/null +++ b/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/Dto/ExternalAuthProviderDto.cs @@ -0,0 +1,50 @@ +using Abp.Application.Services.Dto; +using Abp.AutoMapper; +using ASPBaseOIDC.Authorization.ExternalAuth; +using System.ComponentModel.DataAnnotations; + +namespace ASPBaseOIDC.Application.Authorization.ExternalAuth.Dto; + +[AutoMapFrom(typeof(ExternalAuthProvider))] +[AutoMapTo(typeof(ExternalAuthProvider))] +public class ExternalAuthProviderDto : EntityDto +{ + [Required] + [MaxLength(128)] + public string Name { get; set; } + + [Required] + [MaxLength(64)] + public string ProviderType { get; set; } + + public bool IsEnabled { get; set; } + + [Required] + [MaxLength(512)] + public string Authority { get; set; } + + [Required] + [MaxLength(256)] + public string ClientId { get; set; } + + /// + /// Client secret (masked when returning to client, full when creating/updating) + /// + [MaxLength(512)] + public string ClientSecret { get; set; } + + [MaxLength(512)] + public string Scopes { get; set; } + + [MaxLength(64)] + public string ResponseType { get; set; } + + public bool RequireHttpsMetadata { get; set; } + + [MaxLength(2048)] + public string ClaimMappings { get; set; } + + public int DisplayOrder { get; set; } + + public int? TenantId { get; set; } +} diff --git a/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/Dto/TestConnectionOutput.cs b/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/Dto/TestConnectionOutput.cs new file mode 100644 index 0000000..e23406b --- /dev/null +++ b/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/Dto/TestConnectionOutput.cs @@ -0,0 +1,7 @@ +namespace ASPBaseOIDC.Application.Authorization.ExternalAuth.Dto; + +public class TestConnectionOutput +{ + public bool Success { get; set; } + public string Message { get; set; } +} diff --git a/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/ExternalAuthProviderAppService.cs b/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/ExternalAuthProviderAppService.cs new file mode 100644 index 0000000..8106880 --- /dev/null +++ b/src/ASPBaseOIDC.Application/Authorization/ExternalAuth/ExternalAuthProviderAppService.cs @@ -0,0 +1,212 @@ +using Abp.Application.Services; +using Abp.Authorization; +using Abp.Domain.Repositories; +using Abp.UI; +using ASPBaseOIDC.Application.Authorization.ExternalAuth.Dto; +using ASPBaseOIDC.Authorization; +using ASPBaseOIDC.Authorization.ExternalAuth; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ASPBaseOIDC.Application.Authorization.ExternalAuth; + +/// +/// Application service for managing external authentication providers +/// Requires admin permissions +/// +[AbpAuthorize(PermissionNames.Pages_Users)] // TODO: Create specific permission for external auth +public class ExternalAuthProviderAppService : ApplicationService +{ + private readonly ExternalAuthProviderManager _providerManager; + private readonly IRepository _providerRepository; + + public ExternalAuthProviderAppService( + ExternalAuthProviderManager providerManager, + IRepository providerRepository) + { + _providerManager = providerManager; + _providerRepository = providerRepository; + } + + /// + /// Get all providers (with secrets masked) + /// + public virtual async Task> GetAllProviders() + { + var providers = await _providerManager.GetAllAsync(); + + return providers.Select(p => new ExternalAuthProviderDto + { + Id = p.Id, + Name = p.Name, + ProviderType = p.ProviderType, + IsEnabled = p.IsEnabled, + Authority = p.Authority, + ClientId = p.ClientId, + ClientSecret = MaskSecret(p.ClientSecret), // Mask secret + Scopes = p.Scopes, + ResponseType = p.ResponseType, + RequireHttpsMetadata = p.RequireHttpsMetadata, + ClaimMappings = p.ClaimMappings, + DisplayOrder = p.DisplayOrder, + TenantId = p.TenantId + }).ToList(); + } + + /// + /// Get enabled providers for login page (public - no auth required) + /// + [AbpAllowAnonymous] + public virtual async Task> GetEnabledProviders() + { + var providers = await _providerManager.GetEnabledProvidersAsync(); + + return providers.Select(p => new ExternalAuthProviderDto + { + Id = p.Id, + Name = p.Name, + ProviderType = p.ProviderType, + Authority = p.Authority, + ClientId = p.ClientId, + ClientSecret = null, // Never expose secret publicly + Scopes = p.Scopes, + ResponseType = p.ResponseType, + RequireHttpsMetadata = p.RequireHttpsMetadata, + DisplayOrder = p.DisplayOrder + }).ToList(); + } + + /// + /// Get provider by ID (with secret masked) + /// + public virtual async Task GetProvider(int id) + { + var provider = await _providerManager.GetByIdAsync(id); + + return new ExternalAuthProviderDto + { + Id = provider.Id, + Name = provider.Name, + ProviderType = provider.ProviderType, + IsEnabled = provider.IsEnabled, + Authority = provider.Authority, + ClientId = provider.ClientId, + ClientSecret = MaskSecret(provider.ClientSecret), + Scopes = provider.Scopes, + ResponseType = provider.ResponseType, + RequireHttpsMetadata = provider.RequireHttpsMetadata, + ClaimMappings = provider.ClaimMappings, + DisplayOrder = provider.DisplayOrder, + TenantId = provider.TenantId + }; + } + + /// + /// Create or update provider + /// + public virtual async Task CreateOrUpdateProvider(CreateOrUpdateProviderInput input) + { + ASPBaseOIDC.Authorization.ExternalAuth.ExternalAuthProvider provider; + + if (input.Id.HasValue && input.Id.Value > 0) + { + // Update existing + provider = await _providerRepository.GetAsync(input.Id.Value); + provider.Name = input.Name; + provider.ProviderType = input.ProviderType; + provider.IsEnabled = input.IsEnabled; + provider.Authority = input.Authority; + provider.ClientId = input.ClientId; + + // Only update secret if not masked + if (!IsMaskedSecret(input.ClientSecret)) + { + provider.ClientSecret = input.ClientSecret; + } + + provider.Scopes = input.Scopes; + provider.ResponseType = input.ResponseType; + provider.RequireHttpsMetadata = input.RequireHttpsMetadata; + provider.ClaimMappings = input.ClaimMappings; + provider.DisplayOrder = input.DisplayOrder; + } + else + { + // Create new + provider = new ASPBaseOIDC.Authorization.ExternalAuth.ExternalAuthProvider + { + TenantId = AbpSession.TenantId, + Name = input.Name, + ProviderType = input.ProviderType, + IsEnabled = input.IsEnabled, + Authority = input.Authority, + ClientId = input.ClientId, + ClientSecret = input.ClientSecret, + Scopes = input.Scopes, + ResponseType = input.ResponseType, + RequireHttpsMetadata = input.RequireHttpsMetadata, + ClaimMappings = input.ClaimMappings, + DisplayOrder = input.DisplayOrder + }; + } + + provider = await _providerManager.CreateOrUpdateAsync(provider); + + return await GetProvider(provider.Id); + } + + /// + /// Delete provider + /// + public virtual async Task DeleteProvider(int id) + { + await _providerManager.DeleteAsync(id); + } + + /// + /// Test provider connection (OIDC discovery) + /// + public virtual async Task TestProviderConnection(int id) + { + try + { + var success = await _providerManager.TestConnectionAsync(id); + return new TestConnectionOutput + { + Success = success, + Message = success ? "Connection successful" : "Connection failed" + }; + } + catch (Exception ex) + { + return new TestConnectionOutput + { + Success = false, + Message = $"Connection failed: {ex.Message}" + }; + } + } + + private string MaskSecret(string secret) + { + if (string.IsNullOrEmpty(secret)) + { + return null; + } + + if (secret.Length <= 8) + { + return "********"; + } + + return secret.Substring(0, 4) + "********" + secret.Substring(secret.Length - 4); + } + + private bool IsMaskedSecret(string secret) + { + return !string.IsNullOrEmpty(secret) && secret.Contains("********"); + } +} diff --git a/src/ASPBaseOIDC.Core/ASPBaseOIDC.Core.csproj b/src/ASPBaseOIDC.Core/ASPBaseOIDC.Core.csproj index 7944929..f979cfa 100644 --- a/src/ASPBaseOIDC.Core/ASPBaseOIDC.Core.csproj +++ b/src/ASPBaseOIDC.Core/ASPBaseOIDC.Core.csproj @@ -18,6 +18,8 @@ + + diff --git a/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthProvider.cs b/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthProvider.cs new file mode 100644 index 0000000..e3e18b5 --- /dev/null +++ b/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthProvider.cs @@ -0,0 +1,102 @@ +using Abp.Domain.Entities; +using Abp.Domain.Entities.Auditing; +using System.ComponentModel.DataAnnotations; + +namespace ASPBaseOIDC.Authorization.ExternalAuth; + +/// +/// External authentication provider configuration (OIDC/OAuth2) +/// Similar to Portainer's external auth system +/// +public class ExternalAuthProvider : FullAuditedEntity, IMayHaveTenant +{ + public const int MaxNameLength = 128; + public const int MaxAuthorityLength = 512; + public const int MaxClientIdLength = 256; + public const int MaxClientSecretLength = 512; + public const int MaxScopesLength = 512; + public const int MaxResponseTypeLength = 64; + public const int MaxClaimMappingsLength = 2048; + + /// + /// Tenant ID (null for host) + /// + public int? TenantId { get; set; } + + /// + /// Provider display name (e.g., "Authentik", "Keycloak", "Google") + /// + [Required] + [MaxLength(MaxNameLength)] + public string Name { get; set; } + + /// + /// Provider type: "OIDC", "OAuth2", "SAML" + /// + [Required] + [MaxLength(64)] + public string ProviderType { get; set; } + + /// + /// Enable/disable this provider + /// + public bool IsEnabled { get; set; } + + /// + /// OIDC Authority URL (e.g., https://auth.acme.com/application/o/app1/) + /// + [Required] + [MaxLength(MaxAuthorityLength)] + public string Authority { get; set; } + + /// + /// OAuth2/OIDC Client ID + /// + [Required] + [MaxLength(MaxClientIdLength)] + public string ClientId { get; set; } + + /// + /// OAuth2/OIDC Client Secret (encrypted with SimpleStringCipher) + /// + [Required] + [MaxLength(MaxClientSecretLength)] + public string ClientSecret { get; set; } + + /// + /// Space-separated scopes (e.g., "openid profile email") + /// + [MaxLength(MaxScopesLength)] + public string Scopes { get; set; } + + /// + /// OAuth2 response type (e.g., "code", "id_token", "code id_token") + /// + [MaxLength(MaxResponseTypeLength)] + public string ResponseType { get; set; } + + /// + /// Require HTTPS metadata endpoint + /// + public bool RequireHttpsMetadata { get; set; } + + /// + /// JSON mapping for claims (e.g., {"sub": "sub", "email": "email", "name": "preferred_username"}) + /// + [MaxLength(MaxClaimMappingsLength)] + public string ClaimMappings { get; set; } + + /// + /// Display order in login page (lower = higher priority) + /// + public int DisplayOrder { get; set; } + + public ExternalAuthProvider() + { + IsEnabled = true; + RequireHttpsMetadata = true; + ResponseType = "code"; + Scopes = "openid profile email"; + DisplayOrder = 0; + } +} diff --git a/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthProviderManager.cs b/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthProviderManager.cs new file mode 100644 index 0000000..a20735d --- /dev/null +++ b/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthProviderManager.cs @@ -0,0 +1,262 @@ +using Abp; +using Abp.Configuration; +using Abp.Domain.Repositories; +using Abp.Domain.Services; +using Abp.Runtime.Caching; +using Abp.Runtime.Security; +using Abp.UI; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; + +namespace ASPBaseOIDC.Authorization.ExternalAuth; + +/// +/// Domain service for managing external authentication providers +/// Handles CRUD, encryption, validation, and caching +/// +public class ExternalAuthProviderManager : DomainService +{ + private readonly IRepository _providerRepository; + private readonly ICacheManager _cacheManager; + private readonly IHttpClientFactory _httpClientFactory; + private const string CacheKey = "ExternalAuthProviders"; + + public ExternalAuthProviderManager( + IRepository providerRepository, + ICacheManager cacheManager, + IHttpClientFactory httpClientFactory) + { + _providerRepository = providerRepository; + _cacheManager = cacheManager; + _httpClientFactory = httpClientFactory; + } + + /// + /// Get all providers (with secrets decrypted for admin) + /// + public virtual async Task> GetAllAsync() + { + var providers = await _providerRepository.GetAllListAsync(); + return providers; + } + + /// + /// Get all enabled providers (cached for performance) + /// + public virtual async Task> GetEnabledProvidersAsync() + { + var cache = _cacheManager.GetCache(CacheKey); + var cacheItem = await cache.GetAsync("EnabledProviders", async (key) => + { + var providers = await _providerRepository + .GetAll() + .Where(p => p.IsEnabled) + .OrderBy(p => p.DisplayOrder) + .ToListAsync(); + + return (object)providers; + }); + + return cacheItem as List ?? new List(); + } + + /// + /// Get provider by ID + /// + public virtual async Task GetByIdAsync(int id) + { + var provider = await _providerRepository.FirstOrDefaultAsync(id); + if (provider == null) + { + throw new UserFriendlyException("Provider not found"); + } + return provider; + } + + /// + /// Get provider by name + /// + public virtual async Task GetByNameAsync(string name) + { + return await _providerRepository.FirstOrDefaultAsync(p => p.Name == name); + } + + /// + /// Get provider by issuer (for token validation) + /// + public virtual async Task GetByIssuerAsync(string issuer) + { + var cache = _cacheManager.GetCache(CacheKey); + var providers = await GetEnabledProvidersAsync(); + + return providers.FirstOrDefault(p => + issuer.StartsWith(p.Authority, StringComparison.OrdinalIgnoreCase) || + p.Authority.StartsWith(issuer, StringComparison.OrdinalIgnoreCase) + ); + } + + /// + /// Create or update provider with validation and encryption + /// + public virtual async Task CreateOrUpdateAsync(ExternalAuthProvider provider) + { + ValidateProvider(provider); + + // Encrypt client secret using ABP's SimpleStringCipher + if (!string.IsNullOrEmpty(provider.ClientSecret)) + { + provider.ClientSecret = SimpleStringCipher.Instance.Encrypt(provider.ClientSecret); + } + + if (provider.Id == 0) + { + await _providerRepository.InsertAsync(provider); + } + else + { + await _providerRepository.UpdateAsync(provider); + } + + await CurrentUnitOfWork.SaveChangesAsync(); + ClearCache(); + + return provider; + } + + /// + /// Delete provider (soft delete) + /// + public virtual async Task DeleteAsync(int id) + { + await _providerRepository.DeleteAsync(id); + ClearCache(); + } + + /// + /// Test provider connectivity (OIDC discovery endpoint) + /// + public virtual async Task TestConnectionAsync(int id) + { + var provider = await GetByIdAsync(id); + return await TestConnectionAsync(provider); + } + + /// + /// Test provider connectivity + /// + public virtual async Task TestConnectionAsync(ExternalAuthProvider provider) + { + try + { + var discoveryUrl = provider.Authority.TrimEnd('/') + "/.well-known/openid-configuration"; + var httpClient = _httpClientFactory.CreateClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + + var response = await httpClient.GetAsync(discoveryUrl); + if (!response.IsSuccessStatusCode) + { + return false; + } + + var content = await response.Content.ReadAsStringAsync(); + var doc = JsonDocument.Parse(content); + + // Validate required OIDC fields + return doc.RootElement.TryGetProperty("issuer", out _) && + doc.RootElement.TryGetProperty("authorization_endpoint", out _) && + doc.RootElement.TryGetProperty("token_endpoint", out _); + } + catch + { + return false; + } + } + + /// + /// Decrypt client secret for use (after retrieving from DB) + /// + public virtual string DecryptClientSecret(string encryptedSecret) + { + if (string.IsNullOrEmpty(encryptedSecret)) + { + return null; + } + + try + { + return SimpleStringCipher.Instance.Decrypt(encryptedSecret); + } + catch + { + // If decryption fails, assume it's already plain text (for migration scenarios) + return encryptedSecret; + } + } + + /// + /// Get claim mappings as dictionary + /// + public virtual Dictionary GetClaimMappings(ExternalAuthProvider provider) + { + if (string.IsNullOrEmpty(provider.ClaimMappings)) + { + // Default mappings + return new Dictionary + { + { "sub", "sub" }, + { "email", "email" }, + { "name", "name" }, + { "preferred_username", "preferred_username" } + }; + } + + try + { + return JsonSerializer.Deserialize>(provider.ClaimMappings); + } + catch + { + return new Dictionary(); + } + } + + private void ValidateProvider(ExternalAuthProvider provider) + { + if (string.IsNullOrWhiteSpace(provider.Name)) + { + throw new UserFriendlyException("Provider name is required"); + } + + if (string.IsNullOrWhiteSpace(provider.Authority)) + { + throw new UserFriendlyException("Authority URL is required"); + } + + if (string.IsNullOrWhiteSpace(provider.ClientId)) + { + throw new UserFriendlyException("Client ID is required"); + } + + if (string.IsNullOrWhiteSpace(provider.ClientSecret)) + { + throw new UserFriendlyException("Client Secret is required"); + } + + // Validate Authority is a valid URL + if (!Uri.TryCreate(provider.Authority, UriKind.Absolute, out _)) + { + throw new UserFriendlyException("Authority must be a valid URL"); + } + } + + private void ClearCache() + { + var cache = _cacheManager.GetCache(CacheKey); + cache.Clear(); + } +} diff --git a/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthenticationManager.cs b/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthenticationManager.cs new file mode 100644 index 0000000..524f34b --- /dev/null +++ b/src/ASPBaseOIDC.Core/Authorization/ExternalAuth/ExternalAuthenticationManager.cs @@ -0,0 +1,369 @@ +using Abp; +using Abp.Authorization; +using Abp.Authorization.Users; +using Abp.Configuration; +using Abp.Domain.Repositories; +using Abp.Domain.Services; +using Abp.Domain.Uow; +using Abp.UI; +using ASPBaseOIDC.Authorization.Roles; +using ASPBaseOIDC.Authorization.Users; +using ASPBaseOIDC.Configuration; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Text.Json; +using System.Threading.Tasks; + +namespace ASPBaseOIDC.Authorization.ExternalAuth; + +/// +/// Domain service for external authentication with OIDC/OAuth2 providers +/// Handles token validation, JIT provisioning, and user linking +/// +public class ExternalAuthenticationManager : DomainService +{ + private readonly ExternalAuthProviderManager _providerManager; + private readonly UserManager _userManager; + private readonly RoleManager _roleManager; + private readonly IRepository _userLoginRepository; + private readonly IRepository _userLoginAttemptRepository; + private readonly ISettingManager _settingManager; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public ExternalAuthenticationManager( + ExternalAuthProviderManager providerManager, + UserManager userManager, + RoleManager roleManager, + IRepository userLoginRepository, + IRepository userLoginAttemptRepository, + ISettingManager settingManager, + IHttpClientFactory httpClientFactory, + IUnitOfWorkManager unitOfWorkManager) + { + _providerManager = providerManager; + _userManager = userManager; + _roleManager = roleManager; + _userLoginRepository = userLoginRepository; + _userLoginAttemptRepository = userLoginAttemptRepository; + _settingManager = settingManager; + _httpClientFactory = httpClientFactory; + _unitOfWorkManager = unitOfWorkManager; + } + + /// + /// Authenticate user with external OIDC token + /// Main flow: validate token → find/create user → link provider → return result + /// + public virtual async Task AuthenticateWithExternalTokenAsync( + string providerName, + string idToken, + int? tenantId = null) + { + // 1. Check if external auth is enabled + var isEnabled = await _settingManager.GetSettingValueAsync(AppSettingNames.ExternalAuth.Enabled); + if (!isEnabled) + { + throw new UserFriendlyException("External authentication is disabled"); + } + + // 2. Get provider configuration + var provider = await _providerManager.GetByNameAsync(providerName); + if (provider == null || !provider.IsEnabled) + { + throw new UserFriendlyException("Provider not found or disabled"); + } + + // 3. Validate JWT token + var claims = await ValidateTokenAsync(provider, idToken); + var sub = claims.FirstOrDefault(c => c.Type == "sub")?.Value; + var email = claims.FirstOrDefault(c => c.Type == "email")?.Value; + + if (string.IsNullOrEmpty(sub)) + { + throw new UserFriendlyException("Invalid token: 'sub' claim not found"); + } + + // 4. Find or create user + User user; + using (CurrentUnitOfWork.SetTenantId(tenantId)) + { + user = await FindOrCreateUserAsync(provider, sub, email, claims, tenantId); + + // 5. Register login attempt + await CreateLoginAttemptAsync(user, provider.Name, true, tenantId); + } + + // 6. Return result with original token (passthrough) + return new ExternalAuthenticationResult + { + User = user, + AccessToken = idToken, + Provider = provider.Name, + ExpiresIn = GetTokenExpiration(idToken), + Claims = claims + }; + } + + /// + /// Validate JWT token with provider's JWKS + /// + private async Task> ValidateTokenAsync(ExternalAuthProvider provider, string idToken) + { + try + { + var handler = new JwtSecurityTokenHandler(); + + // First, read token without validation to get issuer + var jwtToken = handler.ReadJwtToken(idToken); + + // Download OIDC configuration + var discoveryUrl = provider.Authority.TrimEnd('/') + "/.well-known/openid-configuration"; + var httpClient = _httpClientFactory.CreateClient(); + var discoveryResponse = await httpClient.GetStringAsync(discoveryUrl); + var discoveryDoc = JsonDocument.Parse(discoveryResponse); + + var jwksUri = discoveryDoc.RootElement.GetProperty("jwks_uri").GetString(); + var issuer = discoveryDoc.RootElement.GetProperty("issuer").GetString(); + + // Download JWKS + var jwksResponse = await httpClient.GetStringAsync(jwksUri); + + // Create validation parameters + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = issuer, + ValidateAudience = true, + ValidAudience = provider.ClientId, + ValidateLifetime = true, + 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()) + { + var keyId = key.GetProperty("kid").GetString(); + if (keyId == kid) + { + 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); + } + } + + return signingKeys; + }, + ClockSkew = TimeSpan.FromMinutes(5) + }; + + // Validate token + var principal = handler.ValidateToken(idToken, validationParameters, out var validatedToken); + return principal.Claims.ToList(); + } + catch (Exception ex) + { + Logger.Error("Token validation failed", ex); + throw new UserFriendlyException("Invalid token: " + ex.Message); + } + } + + /// + /// Find existing user or create new one (JIT provisioning) + /// + private async Task FindOrCreateUserAsync( + ExternalAuthProvider provider, + string sub, + string email, + List claims, + int? tenantId) + { + // 1. Try to find existing link by provider + sub + var existingLogin = await _userLoginRepository.FirstOrDefaultAsync(ul => + ul.LoginProvider == provider.Name && ul.ProviderKey == sub && ul.TenantId == tenantId); + + if (existingLogin != null) + { + var existingUser = await _userManager.FindByIdAsync(existingLogin.UserId.ToString()); + if (existingUser != null) + { + return existingUser; + } + } + + // 2. Check if auto-provisioning is enabled + var autoProvision = await _settingManager.GetSettingValueAsync( + AppSettingNames.ExternalAuth.AutoProvisionUsers); + + if (!autoProvision) + { + throw new UserFriendlyException("User not found and auto-provisioning is disabled"); + } + + // 3. Try to find user by email (merge scenario) + User user = null; + if (!string.IsNullOrEmpty(email)) + { + user = await _userManager.FindByEmailAsync(email); + } + + // 4. Create new user if not found + if (user == null) + { + user = await CreateNewUserAsync(provider, claims, tenantId); + } + + // 5. Link external provider to user + await LinkProviderToUserAsync(user, provider.Name, sub); + + return user; + } + + /// + /// Create new user from external claims + /// + private async Task CreateNewUserAsync( + ExternalAuthProvider provider, + List claims, + int? tenantId) + { + var claimMappings = _providerManager.GetClaimMappings(provider); + + var email = GetClaimValue(claims, claimMappings, "email"); + var name = GetClaimValue(claims, claimMappings, "name") ?? "Unknown"; + var preferredUsername = GetClaimValue(claims, claimMappings, "preferred_username") ?? email?.Split('@')[0]; + + if (string.IsNullOrEmpty(email)) + { + throw new UserFriendlyException("Email claim is required for user provisioning"); + } + + var user = new User + { + TenantId = tenantId, + UserName = preferredUsername ?? Guid.NewGuid().ToString("N").Substring(0, 10), + Name = name, + Surname = GetClaimValue(claims, claimMappings, "family_name") ?? name, + EmailAddress = email, + IsEmailConfirmed = true, // Trust external provider + IsActive = true, + Password = Guid.NewGuid().ToString("N") // Random password (user won't use it) + }; + + user.SetNormalizedNames(); + + var result = await _userManager.CreateAsync(user); + if (!result.Succeeded) + { + throw new UserFriendlyException("Failed to create user: " + string.Join(", ", result.Errors.Select(e => e.Description))); + } + + // Assign default role if configured + var defaultRole = await _settingManager.GetSettingValueAsync(AppSettingNames.ExternalAuth.DefaultRole); + if (!string.IsNullOrEmpty(defaultRole)) + { + var role = await _roleManager.FindByNameAsync(defaultRole); + if (role != null) + { + await _userManager.AddToRoleAsync(user, defaultRole); + } + } + + await CurrentUnitOfWork.SaveChangesAsync(); + return user; + } + + /// + /// Link external provider to user account + /// + private async Task LinkProviderToUserAsync(User user, string providerName, string providerKey) + { + var existingLink = await _userLoginRepository.FirstOrDefaultAsync(ul => + ul.UserId == user.Id && ul.LoginProvider == providerName); + + if (existingLink == null) + { + await _userLoginRepository.InsertAsync(new UserLogin + { + TenantId = user.TenantId, + UserId = user.Id, + LoginProvider = providerName, + ProviderKey = providerKey + }); + + await CurrentUnitOfWork.SaveChangesAsync(); + } + } + + /// + /// Create login attempt record + /// + private async Task CreateLoginAttemptAsync(User user, string providerName, bool success, int? tenantId) + { + await _userLoginAttemptRepository.InsertAsync(new UserLoginAttempt + { + TenantId = tenantId, + UserId = user.Id, + UserNameOrEmailAddress = user.EmailAddress, + ClientIpAddress = "External", + ClientName = providerName, + BrowserInfo = "OIDC", + Result = success ? AbpLoginResultType.Success : AbpLoginResultType.InvalidPassword, + CreationTime = DateTime.UtcNow + }); + + await CurrentUnitOfWork.SaveChangesAsync(); + } + + private string GetClaimValue(List claims, Dictionary mappings, string claimType) + { + if (!mappings.TryGetValue(claimType, out var mappedType)) + { + mappedType = claimType; + } + + return claims.FirstOrDefault(c => c.Type == mappedType)?.Value; + } + + private int GetTokenExpiration(string idToken) + { + try + { + var handler = new JwtSecurityTokenHandler(); + var token = handler.ReadJwtToken(idToken); + var exp = token.ValidTo; + return (int)(exp - DateTime.UtcNow).TotalSeconds; + } + catch + { + return 3600; // Default 1 hour + } + } +} + +/// +/// Result of external authentication +/// +public class ExternalAuthenticationResult +{ + public User User { get; set; } + public string AccessToken { get; set; } + public string Provider { get; set; } + public int ExpiresIn { get; set; } + public List Claims { get; set; } +} diff --git a/src/ASPBaseOIDC.Core/Configuration/AppSettingNames.cs b/src/ASPBaseOIDC.Core/Configuration/AppSettingNames.cs index 2f71fcf..592ab29 100644 --- a/src/ASPBaseOIDC.Core/Configuration/AppSettingNames.cs +++ b/src/ASPBaseOIDC.Core/Configuration/AppSettingNames.cs @@ -3,6 +3,32 @@ public static class AppSettingNames { public const string UiTheme = "App.UiTheme"; + + /// + /// External Authentication Settings + /// + public static class ExternalAuth + { + /// + /// Enable/disable external authentication module globally + /// + public const string Enabled = "App.ExternalAuth.Enabled"; + + /// + /// Allow local login (username/password) alongside external providers + /// + public const string AllowLocalLogin = "App.ExternalAuth.AllowLocalLogin"; + + /// + /// Automatically provision users on first external login (JIT provisioning) + /// + public const string AutoProvisionUsers = "App.ExternalAuth.AutoProvisionUsers"; + + /// + /// Default role name for auto-provisioned users (empty = no role assigned) + /// + public const string DefaultRole = "App.ExternalAuth.DefaultRole"; + } } diff --git a/src/ASPBaseOIDC.Core/Configuration/AppSettingProvider.cs b/src/ASPBaseOIDC.Core/Configuration/AppSettingProvider.cs index f84be98..cf9584a 100644 --- a/src/ASPBaseOIDC.Core/Configuration/AppSettingProvider.cs +++ b/src/ASPBaseOIDC.Core/Configuration/AppSettingProvider.cs @@ -9,7 +9,32 @@ public class AppSettingProvider : SettingProvider { return new[] { - new SettingDefinition(AppSettingNames.UiTheme, "red", scopes: SettingScopes.Application | SettingScopes.Tenant | SettingScopes.User, clientVisibilityProvider: new VisibleSettingClientVisibilityProvider()) + new SettingDefinition(AppSettingNames.UiTheme, "red", scopes: SettingScopes.Application | SettingScopes.Tenant | SettingScopes.User, clientVisibilityProvider: new VisibleSettingClientVisibilityProvider()), + + // External Authentication Settings + new SettingDefinition( + AppSettingNames.ExternalAuth.Enabled, + "false", + scopes: SettingScopes.Application | SettingScopes.Tenant + ), + + new SettingDefinition( + AppSettingNames.ExternalAuth.AllowLocalLogin, + "true", + scopes: SettingScopes.Application | SettingScopes.Tenant + ), + + new SettingDefinition( + AppSettingNames.ExternalAuth.AutoProvisionUsers, + "true", + scopes: SettingScopes.Application | SettingScopes.Tenant + ), + + new SettingDefinition( + AppSettingNames.ExternalAuth.DefaultRole, + "", // Empty = no default role + scopes: SettingScopes.Application | SettingScopes.Tenant + ) }; } } diff --git a/src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/ASPBaseOIDCDbContext.cs b/src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/ASPBaseOIDCDbContext.cs index c475f47..129399e 100644 --- a/src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/ASPBaseOIDCDbContext.cs +++ b/src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/ASPBaseOIDCDbContext.cs @@ -1,4 +1,5 @@ using Abp.Zero.EntityFrameworkCore; +using ASPBaseOIDC.Authorization.ExternalAuth; using ASPBaseOIDC.Authorization.Roles; using ASPBaseOIDC.Authorization.Users; using ASPBaseOIDC.MultiTenancy; @@ -12,6 +13,8 @@ public class ASPBaseOIDCDbContext : AbpZeroDbContext ExternalAuthProviders { get; set; } + public ASPBaseOIDCDbContext(DbContextOptions options) : base(options) { diff --git a/src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/Seed/Host/DefaultExternalAuthProvidersCreator.cs b/src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/Seed/Host/DefaultExternalAuthProvidersCreator.cs new file mode 100644 index 0000000..1483cc6 --- /dev/null +++ b/src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/Seed/Host/DefaultExternalAuthProvidersCreator.cs @@ -0,0 +1,90 @@ +using Abp; +using Abp.Runtime.Security; +using ASPBaseOIDC.Authorization.ExternalAuth; +using System.Linq; +using System.Text.Json; + +namespace ASPBaseOIDC.EntityFrameworkCore.Seed.Host; + +/// +/// Seeds default external authentication providers for development/demo purposes +/// +public class DefaultExternalAuthProvidersCreator +{ + private readonly ASPBaseOIDCDbContext _context; + + public DefaultExternalAuthProvidersCreator(ASPBaseOIDCDbContext context) + { + _context = context; + } + + public void Create() + { + CreateAuthentikProvider(); + CreateKeycloakProvider(); + } + + private void CreateAuthentikProvider() + { + var authentik = _context.ExternalAuthProviders.FirstOrDefault(p => p.Name == "Authentik"); + if (authentik == null) + { + var claimMappings = new + { + sub = "sub", + email = "email", + name = "name", + preferred_username = "preferred_username", + groups = "groups" + }; + + _context.ExternalAuthProviders.Add(new ExternalAuthProvider + { + TenantId = null, // Host provider + Name = "Authentik", + ProviderType = "OIDC", + IsEnabled = false, // Disabled by default - admin must configure and enable + Authority = "http://localhost:9000/application/o/aspbaseoic/", + ClientId = "aspbase-client", + ClientSecret = SimpleStringCipher.Instance.Encrypt("CHANGE_THIS_SECRET"), // Encrypted + Scopes = "openid profile email", + ResponseType = "code", + RequireHttpsMetadata = false, // For local development only + ClaimMappings = JsonSerializer.Serialize(claimMappings), + DisplayOrder = 1 + }); + } + } + + private void CreateKeycloakProvider() + { + var keycloak = _context.ExternalAuthProviders.FirstOrDefault(p => p.Name == "Keycloak"); + if (keycloak == null) + { + var claimMappings = new + { + sub = "sub", + email = "email", + name = "name", + preferred_username = "preferred_username", + groups = "groups" + }; + + _context.ExternalAuthProviders.Add(new ExternalAuthProvider + { + TenantId = null, + Name = "Keycloak", + ProviderType = "OIDC", + IsEnabled = false, + Authority = "http://localhost:8080/realms/master/", + ClientId = "aspbase-client", + ClientSecret = SimpleStringCipher.Instance.Encrypt("CHANGE_THIS_SECRET"), + Scopes = "openid profile email", + ResponseType = "code", + RequireHttpsMetadata = false, + ClaimMappings = JsonSerializer.Serialize(claimMappings), + DisplayOrder = 2 + }); + } + } +} diff --git a/src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/Seed/Host/InitialHostDbBuilder.cs b/src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/Seed/Host/InitialHostDbBuilder.cs index 3bea2c5..4b777f6 100644 --- a/src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/Seed/Host/InitialHostDbBuilder.cs +++ b/src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/Seed/Host/InitialHostDbBuilder.cs @@ -15,6 +15,7 @@ public class InitialHostDbBuilder new DefaultLanguagesCreator(_context).Create(); new HostRoleAndUserCreator(_context).Create(); new DefaultSettingsCreator(_context).Create(); + new DefaultExternalAuthProvidersCreator(_context).Create(); _context.SaveChanges(); } diff --git a/src/ASPBaseOIDC.EntityFrameworkCore/Migrations/20251007010025_AddExternalAuthProviders.Designer.cs b/src/ASPBaseOIDC.EntityFrameworkCore/Migrations/20251007010025_AddExternalAuthProviders.Designer.cs new file mode 100644 index 0000000..393e339 --- /dev/null +++ b/src/ASPBaseOIDC.EntityFrameworkCore/Migrations/20251007010025_AddExternalAuthProviders.Designer.cs @@ -0,0 +1,2051 @@ +// +using System; +using ASPBaseOIDC.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ASPBaseOIDC.Migrations +{ + [DbContext(typeof(ASPBaseOIDCDbContext))] + [Migration("20251007010025_AddExternalAuthProviders")] + partial class AddExternalAuthProviders + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ASPBaseOIDC.Authorization.ExternalAuth.ExternalAuthProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Authority") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ClaimMappings") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ClientSecret") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("DeleterUserId") + .HasColumnType("bigint"); + + b.Property("DeletionTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayOrder") + .HasColumnType("integer"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifierUserId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RequireHttpsMetadata") + .HasColumnType("boolean"); + + b.Property("ResponseType") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Scopes") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ExternalAuthProviders"); + }); + + modelBuilder.Entity("ASPBaseOIDC.Authorization.Roles.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("DeleterUserId") + .HasColumnType("bigint"); + + b.Property("DeletionTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(5000) + .HasColumnType("character varying(5000)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsStatic") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifierUserId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatorUserId"); + + b.HasIndex("DeleterUserId"); + + b.HasIndex("LastModifierUserId"); + + b.HasIndex("TenantId", "NormalizedName"); + + b.ToTable("AbpRoles"); + }); + + modelBuilder.Entity("ASPBaseOIDC.Authorization.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("AuthenticationSource") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("DeleterUserId") + .HasColumnType("bigint"); + + b.Property("DeletionTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmationCode") + .HasMaxLength(328) + .HasColumnType("character varying(328)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("IsLockoutEnabled") + .HasColumnType("boolean"); + + b.Property("IsPhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("IsTwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifierUserId") + .HasColumnType("bigint"); + + b.Property("LockoutEndDateUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedEmailAddress") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("PasswordResetCode") + .HasMaxLength(328) + .HasColumnType("character varying(328)"); + + b.Property("PhoneNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("SecurityStamp") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Surname") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("CreatorUserId"); + + b.HasIndex("DeleterUserId"); + + b.HasIndex("LastModifierUserId"); + + b.HasIndex("TenantId", "NormalizedEmailAddress"); + + b.HasIndex("TenantId", "NormalizedUserName"); + + b.ToTable("AbpUsers"); + }); + + modelBuilder.Entity("ASPBaseOIDC.MultiTenancy.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConnectionString") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("DeleterUserId") + .HasColumnType("bigint"); + + b.Property("DeletionTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EditionId") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifierUserId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenancyName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("CreatorUserId"); + + b.HasIndex("DeleterUserId"); + + b.HasIndex("EditionId"); + + b.HasIndex("LastModifierUserId"); + + b.HasIndex("TenancyName"); + + b.ToTable("AbpTenants"); + }); + + modelBuilder.Entity("Abp.Application.Editions.Edition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("DeleterUserId") + .HasColumnType("bigint"); + + b.Property("DeletionTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifierUserId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.HasKey("Id"); + + b.ToTable("AbpEditions"); + }); + + modelBuilder.Entity("Abp.Application.Features.FeatureSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("character varying(21)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.HasKey("Id"); + + b.ToTable("AbpFeatures"); + + b.HasDiscriminator().HasValue("FeatureSetting"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Abp.Auditing.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ClientName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CustomData") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Exception") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExceptionMessage") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExecutionDuration") + .HasColumnType("integer"); + + b.Property("ExecutionTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ImpersonatorTenantId") + .HasColumnType("integer"); + + b.Property("ImpersonatorUserId") + .HasColumnType("bigint"); + + b.Property("MethodName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Parameters") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("ReturnValue") + .HasColumnType("text"); + + b.Property("ServiceName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ExecutionDuration"); + + b.HasIndex("TenantId", "ExecutionTime"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AbpAuditLogs"); + }); + + modelBuilder.Entity("Abp.Authorization.PermissionSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("character varying(21)"); + + b.Property("IsGranted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name"); + + b.ToTable("AbpPermissions"); + + b.HasDiscriminator().HasValue("PermissionSetting"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Abp.Authorization.Roles.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId", "ClaimType"); + + b.ToTable("AbpRoleClaims"); + }); + + modelBuilder.Entity("Abp.Authorization.Users.UserAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("DeleterUserId") + .HasColumnType("bigint"); + + b.Property("DeletionTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EmailAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifierUserId") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserLinkId") + .HasColumnType("bigint"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("EmailAddress"); + + b.HasIndex("UserName"); + + b.HasIndex("TenantId", "EmailAddress"); + + b.HasIndex("TenantId", "UserId"); + + b.HasIndex("TenantId", "UserName"); + + b.ToTable("AbpUserAccounts"); + }); + + modelBuilder.Entity("Abp.Authorization.Users.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("TenantId", "ClaimType"); + + b.ToTable("AbpUserClaims"); + }); + + modelBuilder.Entity("Abp.Authorization.Users.UserLogin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LoginProvider") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("ProviderKey", "TenantId") + .IsUnique(); + + b.HasIndex("TenantId", "UserId"); + + b.HasIndex("TenantId", "LoginProvider", "ProviderKey"); + + b.ToTable("AbpUserLogins"); + }); + + modelBuilder.Entity("Abp.Authorization.Users.UserLoginAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ClientName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FailReason") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Result") + .HasColumnType("smallint"); + + b.Property("TenancyName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserNameOrEmailAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "TenantId"); + + b.HasIndex("TenancyName", "UserNameOrEmailAddress", "Result"); + + b.ToTable("AbpUserLoginAttempts"); + }); + + modelBuilder.Entity("Abp.Authorization.Users.UserOrganizationUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("OrganizationUnitId") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrganizationUnitId"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AbpUserOrganizationUnits"); + }); + + modelBuilder.Entity("Abp.Authorization.Users.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("TenantId", "RoleId"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AbpUserRoles"); + }); + + modelBuilder.Entity("Abp.Authorization.Users.UserToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExpireDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("Value") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AbpUserTokens"); + }); + + modelBuilder.Entity("Abp.BackgroundJobs.BackgroundJobInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("IsAbandoned") + .HasColumnType("boolean"); + + b.Property("JobArgs") + .IsRequired() + .HasMaxLength(1048576) + .HasColumnType("character varying(1048576)"); + + b.Property("JobType") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("LastTryTime") + .HasColumnType("timestamp with time zone"); + + b.Property("NextTryTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("TryCount") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("IsAbandoned", "NextTryTime"); + + b.ToTable("AbpBackgroundJobs"); + }); + + modelBuilder.Entity("Abp.Configuration.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifierUserId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("TenantId", "Name", "UserId") + .IsUnique(); + + b.ToTable("AbpSettings"); + }); + + modelBuilder.Entity("Abp.DynamicEntityProperties.DynamicEntityProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DynamicPropertyId") + .HasColumnType("integer"); + + b.Property("EntityFullName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DynamicPropertyId"); + + b.HasIndex("EntityFullName", "DynamicPropertyId", "TenantId") + .IsUnique(); + + b.ToTable("AbpDynamicEntityProperties"); + }); + + modelBuilder.Entity("Abp.DynamicEntityProperties.DynamicEntityPropertyValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DynamicEntityPropertyId") + .HasColumnType("integer"); + + b.Property("EntityId") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DynamicEntityPropertyId"); + + b.ToTable("AbpDynamicEntityPropertyValues"); + }); + + modelBuilder.Entity("Abp.DynamicEntityProperties.DynamicProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("InputType") + .HasColumnType("text"); + + b.Property("Permission") + .HasColumnType("text"); + + b.Property("PropertyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PropertyName", "TenantId") + .IsUnique(); + + b.ToTable("AbpDynamicProperties"); + }); + + modelBuilder.Entity("Abp.DynamicEntityProperties.DynamicPropertyValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DynamicPropertyId") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DynamicPropertyId"); + + b.ToTable("AbpDynamicPropertyValues"); + }); + + modelBuilder.Entity("Abp.EntityHistory.EntityChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChangeTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ChangeType") + .HasColumnType("smallint"); + + b.Property("EntityChangeSetId") + .HasColumnType("bigint"); + + b.Property("EntityId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("EntityTypeFullName") + .HasMaxLength(192) + .HasColumnType("character varying(192)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EntityChangeSetId"); + + b.HasIndex("EntityTypeFullName", "EntityId"); + + b.ToTable("AbpEntityChanges"); + }); + + modelBuilder.Entity("Abp.EntityHistory.EntityChangeSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ClientName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExtensionData") + .HasColumnType("text"); + + b.Property("ImpersonatorTenantId") + .HasColumnType("integer"); + + b.Property("ImpersonatorUserId") + .HasColumnType("bigint"); + + b.Property("Reason") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "CreationTime"); + + b.HasIndex("TenantId", "Reason"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AbpEntityChangeSets"); + }); + + modelBuilder.Entity("Abp.EntityHistory.EntityPropertyChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EntityChangeId") + .HasColumnType("bigint"); + + b.Property("NewValue") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("NewValueHash") + .HasColumnType("text"); + + b.Property("OriginalValue") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("OriginalValueHash") + .HasColumnType("text"); + + b.Property("PropertyName") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("PropertyTypeFullName") + .HasMaxLength(192) + .HasColumnType("character varying(192)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EntityChangeId"); + + b.ToTable("AbpEntityPropertyChanges"); + }); + + modelBuilder.Entity("Abp.Localization.ApplicationLanguage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("DeleterUserId") + .HasColumnType("bigint"); + + b.Property("DeletionTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Icon") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDisabled") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifierUserId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name"); + + b.ToTable("AbpLanguages"); + }); + + modelBuilder.Entity("Abp.Localization.ApplicationLanguageText", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LanguageName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifierUserId") + .HasColumnType("bigint"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(67108864) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Source", "LanguageName", "Key"); + + b.ToTable("AbpLanguageTexts"); + }); + + modelBuilder.Entity("Abp.Notifications.NotificationInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("Data") + .HasMaxLength(1048576) + .HasColumnType("character varying(1048576)"); + + b.Property("DataTypeName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EntityId") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("EntityTypeAssemblyQualifiedName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EntityTypeName") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("ExcludedUserIds") + .HasMaxLength(131072) + .HasColumnType("character varying(131072)"); + + b.Property("NotificationName") + .IsRequired() + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("Severity") + .HasColumnType("smallint"); + + b.Property("TargetNotifiers") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("TenantIds") + .HasMaxLength(131072) + .HasColumnType("character varying(131072)"); + + b.Property("UserIds") + .HasMaxLength(131072) + .HasColumnType("character varying(131072)"); + + b.HasKey("Id"); + + b.ToTable("AbpNotifications"); + }); + + modelBuilder.Entity("Abp.Notifications.NotificationSubscriptionInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("EntityId") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("EntityTypeAssemblyQualifiedName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EntityTypeName") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("NotificationName") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("TargetNotifiers") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("NotificationName", "EntityTypeName", "EntityId", "UserId"); + + b.HasIndex("TenantId", "NotificationName", "EntityTypeName", "EntityId", "UserId"); + + b.ToTable("AbpNotificationSubscriptions"); + }); + + modelBuilder.Entity("Abp.Notifications.TenantNotificationInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("Data") + .HasMaxLength(1048576) + .HasColumnType("character varying(1048576)"); + + b.Property("DataTypeName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EntityId") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("EntityTypeAssemblyQualifiedName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EntityTypeName") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("NotificationName") + .IsRequired() + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("Severity") + .HasColumnType("smallint"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("AbpTenantNotifications"); + }); + + modelBuilder.Entity("Abp.Notifications.UserNotificationInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("TargetNotifiers") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("TenantNotificationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "State", "CreationTime"); + + b.ToTable("AbpUserNotifications"); + }); + + modelBuilder.Entity("Abp.Organizations.OrganizationUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(95) + .HasColumnType("character varying(95)"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("DeleterUserId") + .HasColumnType("bigint"); + + b.Property("DeletionTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifierUserId") + .HasColumnType("bigint"); + + b.Property("ParentId") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("TenantId", "Code"); + + b.ToTable("AbpOrganizationUnits"); + }); + + modelBuilder.Entity("Abp.Organizations.OrganizationUnitRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("OrganizationUnitId") + .HasColumnType("bigint"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrganizationUnitId"); + + b.HasIndex("TenantId", "RoleId"); + + b.ToTable("AbpOrganizationUnitRoles"); + }); + + modelBuilder.Entity("Abp.Webhooks.WebhookEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionTime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("WebhookName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("AbpWebhookEvents"); + }); + + modelBuilder.Entity("Abp.Webhooks.WebhookSendAttempt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Response") + .HasColumnType("text"); + + b.Property("ResponseStatusCode") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("WebhookEventId") + .HasColumnType("uuid"); + + b.Property("WebhookSubscriptionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WebhookEventId"); + + b.ToTable("AbpWebhookSendAttempts"); + }); + + modelBuilder.Entity("Abp.Webhooks.WebhookSubscriptionInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Secret") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.Property("WebhookUri") + .IsRequired() + .HasColumnType("text"); + + b.Property("Webhooks") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("AbpWebhookSubscriptions"); + }); + + modelBuilder.Entity("Abp.Application.Features.EditionFeatureSetting", b => + { + b.HasBaseType("Abp.Application.Features.FeatureSetting"); + + b.Property("EditionId") + .HasColumnType("integer"); + + b.HasIndex("EditionId", "Name"); + + b.ToTable("AbpFeatures"); + + b.HasDiscriminator().HasValue("EditionFeatureSetting"); + }); + + modelBuilder.Entity("Abp.MultiTenancy.TenantFeatureSetting", b => + { + b.HasBaseType("Abp.Application.Features.FeatureSetting"); + + b.HasIndex("TenantId", "Name"); + + b.ToTable("AbpFeatures"); + + b.HasDiscriminator().HasValue("TenantFeatureSetting"); + }); + + modelBuilder.Entity("Abp.Authorization.Roles.RolePermissionSetting", b => + { + b.HasBaseType("Abp.Authorization.PermissionSetting"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.HasIndex("RoleId"); + + b.ToTable("AbpPermissions"); + + b.HasDiscriminator().HasValue("RolePermissionSetting"); + }); + + modelBuilder.Entity("Abp.Authorization.Users.UserPermissionSetting", b => + { + b.HasBaseType("Abp.Authorization.PermissionSetting"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasIndex("UserId"); + + b.ToTable("AbpPermissions"); + + b.HasDiscriminator().HasValue("UserPermissionSetting"); + }); + + modelBuilder.Entity("ASPBaseOIDC.Authorization.Roles.Role", b => + { + b.HasOne("ASPBaseOIDC.Authorization.Users.User", "CreatorUser") + .WithMany() + .HasForeignKey("CreatorUserId"); + + b.HasOne("ASPBaseOIDC.Authorization.Users.User", "DeleterUser") + .WithMany() + .HasForeignKey("DeleterUserId"); + + b.HasOne("ASPBaseOIDC.Authorization.Users.User", "LastModifierUser") + .WithMany() + .HasForeignKey("LastModifierUserId"); + + b.Navigation("CreatorUser"); + + b.Navigation("DeleterUser"); + + b.Navigation("LastModifierUser"); + }); + + modelBuilder.Entity("ASPBaseOIDC.Authorization.Users.User", b => + { + b.HasOne("ASPBaseOIDC.Authorization.Users.User", "CreatorUser") + .WithMany() + .HasForeignKey("CreatorUserId"); + + b.HasOne("ASPBaseOIDC.Authorization.Users.User", "DeleterUser") + .WithMany() + .HasForeignKey("DeleterUserId"); + + b.HasOne("ASPBaseOIDC.Authorization.Users.User", "LastModifierUser") + .WithMany() + .HasForeignKey("LastModifierUserId"); + + b.Navigation("CreatorUser"); + + b.Navigation("DeleterUser"); + + b.Navigation("LastModifierUser"); + }); + + modelBuilder.Entity("ASPBaseOIDC.MultiTenancy.Tenant", b => + { + b.HasOne("ASPBaseOIDC.Authorization.Users.User", "CreatorUser") + .WithMany() + .HasForeignKey("CreatorUserId"); + + b.HasOne("ASPBaseOIDC.Authorization.Users.User", "DeleterUser") + .WithMany() + .HasForeignKey("DeleterUserId"); + + b.HasOne("Abp.Application.Editions.Edition", "Edition") + .WithMany() + .HasForeignKey("EditionId"); + + b.HasOne("ASPBaseOIDC.Authorization.Users.User", "LastModifierUser") + .WithMany() + .HasForeignKey("LastModifierUserId"); + + b.Navigation("CreatorUser"); + + b.Navigation("DeleterUser"); + + b.Navigation("Edition"); + + b.Navigation("LastModifierUser"); + }); + + modelBuilder.Entity("Abp.Authorization.Roles.RoleClaim", b => + { + b.HasOne("ASPBaseOIDC.Authorization.Roles.Role", null) + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Abp.Authorization.Users.UserClaim", b => + { + b.HasOne("ASPBaseOIDC.Authorization.Users.User", null) + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Abp.Authorization.Users.UserLogin", b => + { + b.HasOne("ASPBaseOIDC.Authorization.Users.User", null) + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Abp.Authorization.Users.UserRole", b => + { + b.HasOne("ASPBaseOIDC.Authorization.Users.User", null) + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Abp.Authorization.Users.UserToken", b => + { + b.HasOne("ASPBaseOIDC.Authorization.Users.User", null) + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Abp.Configuration.Setting", b => + { + b.HasOne("ASPBaseOIDC.Authorization.Users.User", null) + .WithMany("Settings") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Abp.DynamicEntityProperties.DynamicEntityProperty", b => + { + b.HasOne("Abp.DynamicEntityProperties.DynamicProperty", "DynamicProperty") + .WithMany() + .HasForeignKey("DynamicPropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DynamicProperty"); + }); + + modelBuilder.Entity("Abp.DynamicEntityProperties.DynamicEntityPropertyValue", b => + { + b.HasOne("Abp.DynamicEntityProperties.DynamicEntityProperty", "DynamicEntityProperty") + .WithMany() + .HasForeignKey("DynamicEntityPropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DynamicEntityProperty"); + }); + + modelBuilder.Entity("Abp.DynamicEntityProperties.DynamicPropertyValue", b => + { + b.HasOne("Abp.DynamicEntityProperties.DynamicProperty", "DynamicProperty") + .WithMany("DynamicPropertyValues") + .HasForeignKey("DynamicPropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DynamicProperty"); + }); + + modelBuilder.Entity("Abp.EntityHistory.EntityChange", b => + { + b.HasOne("Abp.EntityHistory.EntityChangeSet", null) + .WithMany("EntityChanges") + .HasForeignKey("EntityChangeSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Abp.EntityHistory.EntityPropertyChange", b => + { + b.HasOne("Abp.EntityHistory.EntityChange", null) + .WithMany("PropertyChanges") + .HasForeignKey("EntityChangeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Abp.Organizations.OrganizationUnit", b => + { + b.HasOne("Abp.Organizations.OrganizationUnit", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Abp.Webhooks.WebhookSendAttempt", b => + { + b.HasOne("Abp.Webhooks.WebhookEvent", "WebhookEvent") + .WithMany() + .HasForeignKey("WebhookEventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WebhookEvent"); + }); + + modelBuilder.Entity("Abp.Application.Features.EditionFeatureSetting", b => + { + b.HasOne("Abp.Application.Editions.Edition", "Edition") + .WithMany() + .HasForeignKey("EditionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Edition"); + }); + + modelBuilder.Entity("Abp.Authorization.Roles.RolePermissionSetting", b => + { + b.HasOne("ASPBaseOIDC.Authorization.Roles.Role", null) + .WithMany("Permissions") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Abp.Authorization.Users.UserPermissionSetting", b => + { + b.HasOne("ASPBaseOIDC.Authorization.Users.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ASPBaseOIDC.Authorization.Roles.Role", b => + { + b.Navigation("Claims"); + + b.Navigation("Permissions"); + }); + + modelBuilder.Entity("ASPBaseOIDC.Authorization.Users.User", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("Permissions"); + + b.Navigation("Roles"); + + b.Navigation("Settings"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Abp.DynamicEntityProperties.DynamicProperty", b => + { + b.Navigation("DynamicPropertyValues"); + }); + + modelBuilder.Entity("Abp.EntityHistory.EntityChange", b => + { + b.Navigation("PropertyChanges"); + }); + + modelBuilder.Entity("Abp.EntityHistory.EntityChangeSet", b => + { + b.Navigation("EntityChanges"); + }); + + modelBuilder.Entity("Abp.Organizations.OrganizationUnit", b => + { + b.Navigation("Children"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ASPBaseOIDC.EntityFrameworkCore/Migrations/20251007010025_AddExternalAuthProviders.cs b/src/ASPBaseOIDC.EntityFrameworkCore/Migrations/20251007010025_AddExternalAuthProviders.cs new file mode 100644 index 0000000..64fa4cb --- /dev/null +++ b/src/ASPBaseOIDC.EntityFrameworkCore/Migrations/20251007010025_AddExternalAuthProviders.cs @@ -0,0 +1,54 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ASPBaseOIDC.Migrations +{ + /// + public partial class AddExternalAuthProviders : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ExternalAuthProviders", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + TenantId = table.Column(type: "integer", nullable: true), + Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + ProviderType = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + IsEnabled = table.Column(type: "boolean", nullable: false), + Authority = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + ClientId = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + ClientSecret = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Scopes = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + ResponseType = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + RequireHttpsMetadata = table.Column(type: "boolean", nullable: false), + ClaimMappings = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), + DisplayOrder = table.Column(type: "integer", nullable: false), + CreationTime = table.Column(type: "timestamp with time zone", nullable: false), + CreatorUserId = table.Column(type: "bigint", nullable: true), + LastModificationTime = table.Column(type: "timestamp with time zone", nullable: true), + LastModifierUserId = table.Column(type: "bigint", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeleterUserId = table.Column(type: "bigint", nullable: true), + DeletionTime = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ExternalAuthProviders", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ExternalAuthProviders"); + } + } +} diff --git a/src/ASPBaseOIDC.EntityFrameworkCore/Migrations/ASPBaseOIDCDbContextModelSnapshot.cs b/src/ASPBaseOIDC.EntityFrameworkCore/Migrations/ASPBaseOIDCDbContextModelSnapshot.cs index 1105011..a358f49 100644 --- a/src/ASPBaseOIDC.EntityFrameworkCore/Migrations/ASPBaseOIDCDbContextModelSnapshot.cs +++ b/src/ASPBaseOIDC.EntityFrameworkCore/Migrations/ASPBaseOIDCDbContextModelSnapshot.cs @@ -22,6 +22,89 @@ namespace ASPBaseOIDC.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("ASPBaseOIDC.Authorization.ExternalAuth.ExternalAuthProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Authority") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ClaimMappings") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ClientSecret") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorUserId") + .HasColumnType("bigint"); + + b.Property("DeleterUserId") + .HasColumnType("bigint"); + + b.Property("DeletionTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayOrder") + .HasColumnType("integer"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifierUserId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RequireHttpsMetadata") + .HasColumnType("boolean"); + + b.Property("ResponseType") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Scopes") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("TenantId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ExternalAuthProviders"); + }); + modelBuilder.Entity("ASPBaseOIDC.Authorization.Roles.Role", b => { b.Property("Id") diff --git a/src/ASPBaseOIDC.Web.Core/Authentication/ExternalAuth/DynamicOidcHandler.cs b/src/ASPBaseOIDC.Web.Core/Authentication/ExternalAuth/DynamicOidcHandler.cs new file mode 100644 index 0000000..df7f19e --- /dev/null +++ b/src/ASPBaseOIDC.Web.Core/Authentication/ExternalAuth/DynamicOidcHandler.cs @@ -0,0 +1,183 @@ +using ASPBaseOIDC.Authorization.ExternalAuth; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Threading.Tasks; + +namespace ASPBaseOIDC.Authentication.ExternalAuth +{ + + /// + /// Dynamic OIDC authentication handler that validates tokens from multiple external providers + /// Automatically detects provider by token issuer and validates accordingly + /// + public class DynamicOidcHandler : AuthenticationHandler + { + private readonly ExternalAuthProviderManager _providerManager; + private readonly IHttpClientFactory _httpClientFactory; + + public DynamicOidcHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock, + ExternalAuthProviderManager providerManager, + IHttpClientFactory httpClientFactory) + : base(options, logger, encoder, clock) + { + _providerManager = providerManager; + _httpClientFactory = httpClientFactory; + } + + protected override async Task HandleAuthenticateAsync() + { + try + { + // 1. Extract token from Authorization header + var token = ExtractToken(); + if (token == null) + { + return AuthenticateResult.NoResult(); + } + + // 2. Read token without validation to get issuer + var handler = new JwtSecurityTokenHandler(); + if (!handler.CanReadToken(token)) + { + return AuthenticateResult.NoResult(); + } + + var jwtToken = handler.ReadJwtToken(token); + var issuer = jwtToken.Issuer; + + // 3. Find provider by issuer (cached) + var provider = await _providerManager.GetByIssuerAsync(issuer); + if (provider == null || !provider.IsEnabled) + { + Logger.LogWarning($"No enabled provider found for issuer: {issuer}"); + return AuthenticateResult.NoResult(); + } + + // 4. Validate token with provider configuration + var validationParams = await BuildValidationParametersAsync(provider); + var principal = handler.ValidateToken(token, validationParams, out var validatedToken); + + // 5. Create authentication ticket + var ticket = new AuthenticationTicket(principal, Scheme.Name); + return AuthenticateResult.Success(ticket); + } + catch (SecurityTokenException ex) + { + Logger.LogWarning(ex, "Token validation failed"); + return AuthenticateResult.Fail(ex.Message); + } + catch (Exception ex) + { + Logger.LogError(ex, "Unexpected error during authentication"); + return AuthenticateResult.Fail("Authentication failed"); + } + } + + /// + /// Extract bearer token from Authorization header + /// + private string ExtractToken() + { + var authorization = Request.Headers["Authorization"].ToString(); + if (string.IsNullOrEmpty(authorization)) + { + return null; + } + + if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + return authorization.Substring("Bearer ".Length).Trim(); + } + + return null; + } + + /// + /// Build token validation parameters for specific provider + /// + private async Task BuildValidationParametersAsync( + ASPBaseOIDC.Authorization.ExternalAuth.ExternalAuthProvider provider) + { + // Download OIDC discovery document + var discoveryUrl = provider.Authority.TrimEnd('/') + "/.well-known/openid-configuration"; + var httpClient = _httpClientFactory.CreateClient(); + + var discoveryResponse = await httpClient.GetStringAsync(discoveryUrl); + var discoveryDoc = JsonDocument.Parse(discoveryResponse); + + var issuer = discoveryDoc.RootElement.GetProperty("issuer").GetString(); + var jwksUri = discoveryDoc.RootElement.GetProperty("jwks_uri").GetString(); + + // Download JWKS + var jwksResponse = await httpClient.GetStringAsync(jwksUri); + var jwks = JsonDocument.Parse(jwksResponse); + + return new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = issuer, + ValidateAudience = true, + ValidAudience = provider.ClientId, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKeyResolver = (token, securityToken, kid, parameters) => + { + return ResolveSigningKeys(jwks, kid); + }, + ClockSkew = TimeSpan.FromMinutes(5) + }; + } + + /// + /// Resolve signing keys from JWKS + /// + private IEnumerable ResolveSigningKeys(JsonDocument jwks, string kid) + { + var keys = jwks.RootElement.GetProperty("keys"); + var signingKeys = new List(); + + foreach (var key in keys.EnumerateArray()) + { + var keyId = key.GetProperty("kid").GetString(); + if (keyId == kid) + { + var kty = key.GetProperty("kty").GetString(); + + if (kty == "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); + } + } + } + + return signingKeys; + } + } +} \ No newline at end of file diff --git a/src/ASPBaseOIDC.Web.Core/Controllers/TokenAuthController.cs b/src/ASPBaseOIDC.Web.Core/Controllers/TokenAuthController.cs index eaa790d..181ca7d 100644 --- a/src/ASPBaseOIDC.Web.Core/Controllers/TokenAuthController.cs +++ b/src/ASPBaseOIDC.Web.Core/Controllers/TokenAuthController.cs @@ -2,8 +2,10 @@ using Abp.Authorization.Users; using Abp.MultiTenancy; using Abp.Runtime.Security; +using ASPBaseOIDC.Application.Authorization.ExternalAuth.Dto; using ASPBaseOIDC.Authentication.JwtBearer; using ASPBaseOIDC.Authorization; +using ASPBaseOIDC.Authorization.ExternalAuth; using ASPBaseOIDC.Authorization.Users; using ASPBaseOIDC.Models.TokenAuth; using ASPBaseOIDC.MultiTenancy; @@ -24,17 +26,20 @@ namespace ASPBaseOIDC.Controllers private readonly ITenantCache _tenantCache; private readonly AbpLoginResultTypeHelper _abpLoginResultTypeHelper; private readonly TokenAuthConfiguration _configuration; + private readonly ExternalAuthenticationManager _externalAuthManager; public TokenAuthController( LogInManager logInManager, ITenantCache tenantCache, AbpLoginResultTypeHelper abpLoginResultTypeHelper, - TokenAuthConfiguration configuration) + TokenAuthConfiguration configuration, + ExternalAuthenticationManager externalAuthManager) { _logInManager = logInManager; _tenantCache = tenantCache; _abpLoginResultTypeHelper = abpLoginResultTypeHelper; _configuration = configuration; + _externalAuthManager = externalAuthManager; } [HttpPost] @@ -57,6 +62,31 @@ namespace ASPBaseOIDC.Controllers }; } + /// + /// Authenticate with external OIDC/OAuth2 provider (Authentik, Keycloak, etc.) + /// Passthrough approach: validates external token and returns it as-is + /// + [HttpPost] + [AbpAllowAnonymous] + public async Task AuthenticateExternal([FromBody] ExternalAuthModel model) + { + // Authenticate with external provider (validates token, provisions user if needed) + var result = await _externalAuthManager.AuthenticateWithExternalTokenAsync( + model.ProviderName, + model.IdToken, + AbpSession.TenantId + ); + + // Return original external token (passthrough approach) + return new AuthenticateResultModel + { + AccessToken = result.AccessToken, // Passthrough external token + EncryptedAccessToken = GetEncryptedAccessToken(result.AccessToken), + ExpireInSeconds = result.ExpiresIn, + UserId = result.User.Id + }; + } + private string GetTenancyNameOrNull() { if (!AbpSession.TenantId.HasValue) diff --git a/src/ASPBaseOIDC.Web.Host/Startup/AuthConfigurer.cs b/src/ASPBaseOIDC.Web.Host/Startup/AuthConfigurer.cs index 218f461..9f5070b 100644 --- a/src/ASPBaseOIDC.Web.Host/Startup/AuthConfigurer.cs +++ b/src/ASPBaseOIDC.Web.Host/Startup/AuthConfigurer.cs @@ -1,5 +1,7 @@ using Abp.Runtime.Security; +using ASPBaseOIDC.Authentication.ExternalAuth; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; @@ -20,7 +22,9 @@ namespace ASPBaseOIDC.Web.Host.Startup { options.DefaultAuthenticateScheme = "JwtBearer"; options.DefaultChallengeScheme = "JwtBearer"; - }).AddJwtBearer("JwtBearer", options => + }) + // Local JWT Bearer authentication (username/password) + .AddJwtBearer("JwtBearer", options => { options.Audience = configuration["Authentication:JwtBearer:Audience"]; @@ -49,6 +53,19 @@ namespace ASPBaseOIDC.Web.Host.Startup { OnMessageReceived = QueryStringTokenResolver }; + }) + // Dynamic OIDC authentication for external providers (Authentik, Keycloak, etc.) + .AddScheme("DynamicOidc", options => + { + // Options are handled dynamically by DynamicOidcHandler + }); + + // Configure authorization to accept both local and external tokens + services.AddAuthorization(options => + { + options.DefaultPolicy = new AuthorizationPolicyBuilder("JwtBearer", "DynamicOidc") + .RequireAuthenticatedUser() + .Build(); }); } } diff --git a/src/ASPBaseOIDC.Web.Host/appsettings.json b/src/ASPBaseOIDC.Web.Host/appsettings.json index 407cc8d..5c9c444 100644 --- a/src/ASPBaseOIDC.Web.Host/appsettings.json +++ b/src/ASPBaseOIDC.Web.Host/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "Default": "User Id=postgres;Password=admin57$;Server=134.199.235.213;Port=5432;Database=isa_base_auth_db" + "Default": "User Id=postgres;Password=admin57$;Server=134.199.235.213;Port=5432;Database=isa_base_auth" }, "App": { "ServerRootAddress": "https://localhost:44311/",