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