using System; using System.Linq; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using GB5Shared.Connection; using GB5Shared.DTO.Framework.Login; using GB5Shared.Query.FrameWork.User; using GB5Shared.QueryExecutor; using Microsoft.Extensions.Logging; using static GB5Shared.GB5Constant.Constant; namespace GB5Shared.Auth.Jwt { // The real implementation of the bridge BaseEndPoint.GetLoginDTOFromRequestAsync calls when a // registered AddJwtBearer scheme has already validated a Keycloak token. Lives in GB5Shared — // not FrameworkBLL — specifically so all five hosts (FrameworkSL + BusinessHost/EngagementHost/ // PlatformHost/HRFinanceHost) can register the same implementation: none of the four module // hosts reference FrameworkBLL/FrameworkDAL (each is scoped to its own module set), so a // resolver built on FrameworkBLL.AuthenticationBLL could only ever run in FrameworkSL. // // Reuses UserQB.USER_DETAIL/PG_USER_DETAIL — the SAME, already-proven, tenant-filtered // (WHERE a.TENANTID=@clientid) query AuthenticationDAL.GetUser uses for every other login // path today — via IQueryExecutor/IApplicationConnection, both already in GB5Shared. This is // a direct DB lookup, not a call into AuthenticationBLL.AuthenticateUserViaKeyCloak: no // password check happens here (correct — the caller already cryptographically verified the // token's signature before HttpContext.User was ever populated, which is what makes this safe // unlike KeyCloakService.cs's pre-fix state), and no MSESSIONSTORE row is written (a // deliberate, documented simplification — see the class-level note in the plan; add it later // without changing this bridge's shape if session bookkeeping turns out to matter here). public class KeycloakLoginDTOResolver : IKeycloakLoginDTOResolver { private readonly IApplicationConnection _connection; private readonly IQueryExecutor _queryExecutor; private readonly ILogger _logger; public KeycloakLoginDTOResolver( IApplicationConnection connection, IQueryExecutor queryExecutor, ILogger logger) { _connection = connection; _queryExecutor = queryExecutor; _logger = logger; } public async Task ResolveAsync(ClaimsPrincipal principal, CancellationToken ct = default) { string? username = principal.FindFirst("preferred_username")?.Value; if (string.IsNullOrWhiteSpace(username)) { _logger.LogWarning("KeycloakLoginDTOResolver: token has no preferred_username claim"); return null; } // Every Claim carries the Issuer it was validated against (set by JwtSecurityTokenHandler // from the token's own "iss") — this is how we learn which realm authenticated this // request without needing a second header/claim just for that. string? issuer = principal.Claims.FirstOrDefault()?.Issuer; string? realm = ExtractRealmFromIssuer(issuer); if (string.IsNullOrWhiteSpace(realm)) { _logger.LogWarning("KeycloakLoginDTOResolver: could not extract a realm from token issuer '{Issuer}'", issuer); return null; } var serverConfig = await _connection.DatabaseConnectionObjectByKeycloakRealmCached(realm); if (serverConfig == null) { _logger.LogWarning("KeycloakLoginDTOResolver: no tenant configured for Keycloak realm '{Realm}'", realm); return null; } // Minimal LoginDTO carrying only what USER_DETAIL/PG_USER_DETAIL's own tenant/connection // resolution needs — QueryExecutor resolves the physical connection from // ConnectionDatabaseName/DatabaseName, mirroring AuthenticateUserViaKeyCloak's own setup. var lookupLogin = new LoginDTO { DatabaseName = serverConfig.ConnectionName, ConnectionDatabaseName = serverConfig.ConnectionName, DatabaseType = serverConfig.DbType, ClientId = serverConfig.ClientId, ModeOfOperation = 3, // "don't log at lookup time" — matches every other login path's convention }; string query = serverConfig.DbType == DBType.SQL ? UserQB.USER_DETAIL : UserQB.PG_USER_DETAIL; KeycloakUserLookupDTO? user; try { user = await _queryExecutor.QuerySingleAsync( lookupLogin, query, new { usercode = username, clientid = serverConfig.ClientId, developerid = -1 }, cancellationToken: ct); } catch (Exception ex) { _logger.LogWarning(ex, "KeycloakLoginDTOResolver: MUSER lookup failed for user '{UserCode}' tenant '{ConnectionName}'", username, serverConfig.ConnectionName); return null; } if (user == null) { _logger.LogWarning( "KeycloakLoginDTOResolver: no MUSER row for UserCode '{UserCode}' in tenant '{ConnectionName}' (realm '{Realm}')", username, serverConfig.ConnectionName, realm); return null; } return MapToLoginDTO(user, serverConfig.ConnectionName, serverConfig.DbType, serverConfig.ClientId); } private static string? ExtractRealmFromIssuer(string? issuer) { if (string.IsNullOrWhiteSpace(issuer)) return null; const string marker = "/realms/"; int idx = issuer.IndexOf(marker, StringComparison.OrdinalIgnoreCase); if (idx < 0) return null; return issuer.Substring(idx + marker.Length).Trim('/'); } private static LoginDTO MapToLoginDTO(KeycloakUserLookupDTO u, string connectionName, byte databaseType, int clientId) { return new LoginDTO { UserCode = u.UserCode ?? "", UserName = u.UserName, UserId = u.UserId, RoleId = u.RoleId, ClientId = clientId, DatabaseName = connectionName, ConnectionDatabaseName = connectionName, DatabaseType = databaseType, ModeOfOperation = u.CheckModeofOperation, WorkOUId = u.UserWorkOuId, OuCode = u.UserWorkOuCode, OuName = u.UserWorkOuName, WorkPeriodId = u.UserWorkPeriodId, WorkPartyBranchId = u.UserWorkPartyBranchId, WorkPartyId = u.UserWorkPartyId, WorkStoreId = u.UserWorkStoreId, WorkDate = u.UserWorkDate, WorkFinanceBookId = u.WorkFinanceBookId, UserCriteriaConfigId = u.UserCriteriaConfigId, DateFormat = u.UserDateFormat ?? "dd/MM/yyyy", TimeFormat = u.UserTimeFormat ?? "HH:MM", CurrencyFormat = u.UserCurrencyFormat ?? "#,##,###.##", QuantityFormat = u.UserQuantityFormat ?? "###0.00", Delimiter = u.UserDelimiter ?? ",", UserLoginName = u.UserLoginName, UserPrimaryMailId = u.UserPrimaryMail, TimeZoneId = u.TimeZoneId, TimeZone = u.TimeZone, TimeZoneDisplayName = u.TimeZoneDisplayName, CounterOperationId = u.CounterOperationId, SelectlistOperationType = u.SelectlistOperationType, ExpiryTime = u.ExpiryTime, GraceTime = u.GraceTime, IsIpBasedCheckingRequired = u.IsIpBasedCheckingRequired, AttachmentOption = u.TempAttachmentOption, SessionId = 0, }; } } }