From ad0d9e41bae5feed5c8581a63fba810ef88336ce Mon Sep 17 00:00:00 2001 From: Thanakarn Klangkasame <77600906+Simulationable@users.noreply.github.com> Date: Sun, 5 Oct 2025 17:24:30 +0700 Subject: [PATCH] Fix Access/Refres Token --- .../Controllers/AuthenticationController.cs | 11 ++- .../Middleware/StrictTenantGuardMiddleware.cs | 1 + .../Repositories/ITenantRepository.cs | 3 + .../Authentications/IssueTokenPairUseCase.cs | 97 +++++++------------ .../UseCases/Authentications/LoginUseCase.cs | 84 ++++++++++------ .../IssueTokenPair/IssueTokenPairRequest.cs | 1 + .../Authentications/Login/LoginRequest.cs | 1 - .../Authentications/Login/LoginResponse.cs | 5 +- .../Repositories/TenantRepository.cs | 71 ++++++++++++-- .../Repositories/UserRepository.cs | 36 +++---- .../Security/JwtFactory.cs | 14 +-- .../Tenancy/DefaultTenantResolver.cs | 10 +- 12 files changed, 191 insertions(+), 143 deletions(-) diff --git a/AMREZ.EOP.API/Controllers/AuthenticationController.cs b/AMREZ.EOP.API/Controllers/AuthenticationController.cs index 9f7e5ce..1c29e0f 100644 --- a/AMREZ.EOP.API/Controllers/AuthenticationController.cs +++ b/AMREZ.EOP.API/Controllers/AuthenticationController.cs @@ -74,7 +74,7 @@ public class AuthenticationController : ControllerBase new(ClaimTypes.NameIdentifier, res.UserId.ToString()), new(ClaimTypes.Name, res.Email), new(ClaimTypes.Email, res.Email), - new("tenant", res.TenantId) + new("tenant", res.TenantKey) }; var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthPolicies.Scheme)); @@ -83,7 +83,8 @@ public class AuthenticationController : ControllerBase var tokenPair = await _issueTokens.ExecuteAsync(new IssueTokenPairRequest() { UserId = res.UserId, - Tenant = res.TenantId, + TenantId = res.TenantId, + Tenant = res.TenantKey, Email = res.Email }, ct); @@ -120,7 +121,7 @@ public class AuthenticationController : ControllerBase if (string.IsNullOrWhiteSpace(raw)) return Unauthorized(new { message = "Missing refresh token" }); - var res = await _refresh.ExecuteAsync(body, ct); + var res = await _refresh.ExecuteAsync(new RefreshRequest { RefreshToken = raw }, ct); if (res is null) return Unauthorized(new { message = "Invalid/expired refresh token" }); if (!string.IsNullOrWhiteSpace(res.RefreshToken)) @@ -131,8 +132,8 @@ public class AuthenticationController : ControllerBase new CookieOptions { HttpOnly = true, - Secure = true, - SameSite = SameSiteMode.Strict, + Secure = false, + SameSite = SameSiteMode.None, Expires = res.RefreshExpiresAt?.UtcDateTime }); } diff --git a/AMREZ.EOP.API/Middleware/StrictTenantGuardMiddleware.cs b/AMREZ.EOP.API/Middleware/StrictTenantGuardMiddleware.cs index 97a2a68..34ed5b8 100644 --- a/AMREZ.EOP.API/Middleware/StrictTenantGuardMiddleware.cs +++ b/AMREZ.EOP.API/Middleware/StrictTenantGuardMiddleware.cs @@ -16,6 +16,7 @@ public sealed class StrictTenantGuardOptions ("GET", "/swagger"), ("GET", "/swagger/index.html"), ("GET", "/swagger/"), + ("POST", "/api/authentication/login") }; public Regex SlugRegex { get; set; } = new(@"^[a-z0-9\-]{1,64}$", RegexOptions.Compiled); } diff --git a/AMREZ.EOP.Abstractions/Infrastructures/Repositories/ITenantRepository.cs b/AMREZ.EOP.Abstractions/Infrastructures/Repositories/ITenantRepository.cs index 59d2462..67f1405 100644 --- a/AMREZ.EOP.Abstractions/Infrastructures/Repositories/ITenantRepository.cs +++ b/AMREZ.EOP.Abstractions/Infrastructures/Repositories/ITenantRepository.cs @@ -18,4 +18,7 @@ public interface ITenantRepository Task AddBaseDomainAsync(string baseDomain, CancellationToken ct = default); Task RemoveBaseDomainAsync(string baseDomain, CancellationToken ct = default); + + Task GetTenantKeyByTenantIdAsync(Guid tenantId, CancellationToken ct = default); + Task> MapTenantKeysAsync(IEnumerable tenantIds, CancellationToken ct = default); } \ No newline at end of file diff --git a/AMREZ.EOP.Application/UseCases/Authentications/IssueTokenPairUseCase.cs b/AMREZ.EOP.Application/UseCases/Authentications/IssueTokenPairUseCase.cs index 3994246..16566b9 100644 --- a/AMREZ.EOP.Application/UseCases/Authentications/IssueTokenPairUseCase.cs +++ b/AMREZ.EOP.Application/UseCases/Authentications/IssueTokenPairUseCase.cs @@ -15,92 +15,67 @@ namespace AMREZ.EOP.Application.UseCases.Authentications; public sealed class IssueTokenPairUseCase : IIssueTokenPairUseCase { - private readonly ITenantResolver _tenantResolver; // ctx/key for UoW - private readonly ITenantRepository _tenants; // get TenantId (Guid) private readonly IUserRepository _users; private readonly IJwtFactory _jwt; private readonly IHttpContextAccessor _http; - private readonly IUnitOfWork _uow; private const int RefreshDays = 14; public IssueTokenPairUseCase( - ITenantResolver resolver, - ITenantRepository tenants, IUserRepository users, IJwtFactory jwt, - IHttpContextAccessor http, - IUnitOfWork uow) + IHttpContextAccessor http) { - _tenantResolver = resolver; - _tenants = tenants; _users = users; _jwt = jwt; _http = http; - _uow = uow; } public async Task ExecuteAsync(IssueTokenPairRequest request, CancellationToken ct = default) { var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext"); - var tenantCtx = _tenantResolver.Resolve(http, request); - if (tenantCtx is null) throw new InvalidOperationException("Cannot resolve tenant context"); + var tenantId = request.TenantId; - await _uow.BeginAsync(tenantCtx, IsolationLevel.ReadCommitted, ct); + var refreshRaw = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); + var refreshHash = Sha256(refreshRaw); + var now = DateTimeOffset.UtcNow; - try + var session = await _users.CreateSessionAsync(new UserSession { - var tn = await _tenants.GetAsync(tenantCtx.Id, ct); - var tenantId = tn.TenantId; + Id = Guid.NewGuid(), + TenantId = tenantId, + UserId = request.UserId, + RefreshTokenHash = refreshHash, + IssuedAt = now, + ExpiresAt = now.AddDays(RefreshDays), + DeviceId = http.Request.Headers["X-Device-Id"].FirstOrDefault(), + UserAgent = http.Request.Headers["User-Agent"].FirstOrDefault(), + IpAddress = http.Connection.RemoteIpAddress?.ToString() + }, ct); - var refreshRaw = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); - var refreshHash = Sha256(refreshRaw); - var now = DateTimeOffset.UtcNow; + var tv = await _users.GetTenantTokenVersionAsync(tenantId, ct); + var sstamp = await _users.GetUserSecurityStampAsync(request.UserId, ct) ?? string.Empty; - var session = await _users.CreateSessionAsync(new UserSession - { - Id = Guid.NewGuid(), - TenantId = tenantId, - UserId = request.UserId, - RefreshTokenHash = refreshHash, - IssuedAt = now, - ExpiresAt = now.AddDays(RefreshDays), - DeviceId = http.Request.Headers["X-Device-Id"].FirstOrDefault(), - UserAgent = http.Request.Headers["User-Agent"].FirstOrDefault(), - IpAddress = http.Connection.RemoteIpAddress?.ToString() - }, ct); - - // 6) tv / sstamp - var tv = await _users.GetTenantTokenVersionAsync(tenantId, ct); - var sstamp = await _users.GetUserSecurityStampAsync(request.UserId, ct) ?? string.Empty; - - var claims = new List - { - new("sub", request.UserId.ToString()), - new("tenant_id", tenantId.ToString()), - new("sid", session.Id.ToString()), - new("jti", Guid.NewGuid().ToString("N")), - new("tv", tv), - new("sstamp", sstamp) - }; - var (access, accessExp) = _jwt.CreateAccessToken(claims); - - await _uow.CommitAsync(ct); - - return new IssueTokenPairResponse - { - AccessToken = access, - AccessExpiresAt = accessExp, - RefreshToken = refreshRaw, // raw ออกให้ client - RefreshExpiresAt = session.ExpiresAt - }; - } - catch + var claims = new List { - await _uow.RollbackAsync(ct); - throw; - } + new("sub", request.UserId.ToString()), + new("tenant_id", tenantId.ToString()), + new("sid", session.Id.ToString()), + new("jti", Guid.NewGuid().ToString("N")), + new("tv", tv), + new("sstamp", sstamp) + }; + + var (access, accessExp) = _jwt.CreateAccessToken(claims); + + return new IssueTokenPairResponse + { + AccessToken = access, + AccessExpiresAt = accessExp, + RefreshToken = refreshRaw, // ส่ง raw กลับให้ client + RefreshExpiresAt = session.ExpiresAt + }; } private static string Sha256(string raw) diff --git a/AMREZ.EOP.Application/UseCases/Authentications/LoginUseCase.cs b/AMREZ.EOP.Application/UseCases/Authentications/LoginUseCase.cs index 4452690..135cea3 100644 --- a/AMREZ.EOP.Application/UseCases/Authentications/LoginUseCase.cs +++ b/AMREZ.EOP.Application/UseCases/Authentications/LoginUseCase.cs @@ -7,44 +7,68 @@ using AMREZ.EOP.Abstractions.Security; using AMREZ.EOP.Contracts.DTOs.Authentications.Login; using Microsoft.AspNetCore.Http; -namespace AMREZ.EOP.Application.UseCases.Authentications; - -public sealed class LoginUseCase : ILoginUseCase +namespace AMREZ.EOP.Application.UseCases.Authentications { - private readonly ITenantResolver _tenantResolver; - private readonly IUserRepository _users; - private readonly IPasswordHasher _hasher; - private readonly IHttpContextAccessor _http; - private readonly IUnitOfWork _uow; - - public LoginUseCase(ITenantResolver r, IUserRepository u, IPasswordHasher h, IHttpContextAccessor http, IUnitOfWork uow) - { _tenantResolver = r; _users = u; _hasher = h; _http = http; _uow = uow; } - - public async Task ExecuteAsync(LoginRequest request, CancellationToken ct = default) + public sealed class LoginUseCase : ILoginUseCase { - var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext"); - var tenant = _tenantResolver.Resolve(http, request); - if (tenant is null) return null; + private readonly ITenantResolver _tenantResolver; + private readonly IUserRepository _users; + private readonly ITenantRepository _tenants; + private readonly IPasswordHasher _hasher; + private readonly IHttpContextAccessor _http; + private readonly IUnitOfWork _uow; - await _uow.BeginAsync(tenant, IsolationLevel.ReadCommitted, ct); - try + public LoginUseCase( + ITenantResolver tenantResolver, + IUserRepository users, + ITenantRepository tenants, + IPasswordHasher hasher, + IHttpContextAccessor http, + IUnitOfWork uow) { + _tenantResolver = tenantResolver; + _users = users; + _tenants = tenants; + _hasher = hasher; + _http = http; + _uow = uow; + } + + public async Task ExecuteAsync(LoginRequest request, CancellationToken ct = default) + { + var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext"); + + var platform = _tenantResolver.Resolve(http, "@platform"); + if (platform is null) return null; + var email = request.Email.Trim().ToLowerInvariant(); - var user = await _users.FindActiveByEmailAsync(email, ct); - if (user is null || !_hasher.Verify(request.Password, user.PasswordHash)) + + await _uow.BeginAsync(platform, IsolationLevel.ReadCommitted, ct); + try + { + var user = await _users.FindActiveByEmailAsync(email, ct); + if (user is null || !_hasher.Verify(request.Password, user.PasswordHash)) + { + await _uow.RollbackAsync(ct); + return null; + } + + var tenantKey = await _tenants.GetTenantKeyByTenantIdAsync(user.TenantId, ct); + if (string.IsNullOrWhiteSpace(tenantKey)) + { + await _uow.RollbackAsync(ct); + return null; + } + + await _uow.CommitAsync(ct); + + return new LoginResponse(user.Id, user.TenantId, email, tenantKey); + } + catch { await _uow.RollbackAsync(ct); - return null; + throw; } - - await _uow.CommitAsync(ct); - - return new LoginResponse(user.Id , email, tenant.Id); - } - catch - { - await _uow.RollbackAsync(ct); - throw; } } } \ No newline at end of file diff --git a/AMREZ.EOP.Contracts/DTOs/Authentications/IssueTokenPair/IssueTokenPairRequest.cs b/AMREZ.EOP.Contracts/DTOs/Authentications/IssueTokenPair/IssueTokenPairRequest.cs index e6a9a81..9aaeb62 100644 --- a/AMREZ.EOP.Contracts/DTOs/Authentications/IssueTokenPair/IssueTokenPairRequest.cs +++ b/AMREZ.EOP.Contracts/DTOs/Authentications/IssueTokenPair/IssueTokenPairRequest.cs @@ -3,6 +3,7 @@ namespace AMREZ.EOP.Contracts.DTOs.Authentications.IssueTokenPair; public sealed class IssueTokenPairRequest { public Guid UserId { get; init; } + public Guid TenantId { get; init; } = default!; public string Tenant { get; init; } = default!; public string Email { get; init; } = default!; } \ No newline at end of file diff --git a/AMREZ.EOP.Contracts/DTOs/Authentications/Login/LoginRequest.cs b/AMREZ.EOP.Contracts/DTOs/Authentications/Login/LoginRequest.cs index 4f2e280..a217440 100644 --- a/AMREZ.EOP.Contracts/DTOs/Authentications/Login/LoginRequest.cs +++ b/AMREZ.EOP.Contracts/DTOs/Authentications/Login/LoginRequest.cs @@ -2,7 +2,6 @@ namespace AMREZ.EOP.Contracts.DTOs.Authentications.Login; public sealed class LoginRequest { - public string? Tenant { get; set; } public string Email { get; set; } = default!; public string Password { get; set; } = default!; } \ No newline at end of file diff --git a/AMREZ.EOP.Contracts/DTOs/Authentications/Login/LoginResponse.cs b/AMREZ.EOP.Contracts/DTOs/Authentications/Login/LoginResponse.cs index dd73c05..8b0bf6a 100644 --- a/AMREZ.EOP.Contracts/DTOs/Authentications/Login/LoginResponse.cs +++ b/AMREZ.EOP.Contracts/DTOs/Authentications/Login/LoginResponse.cs @@ -1,7 +1,8 @@ namespace AMREZ.EOP.Contracts.DTOs.Authentications.Login; public sealed record LoginResponse( - Guid UserId, + Guid UserId, + Guid TenantId, string Email, - string TenantId + string TenantKey ); \ No newline at end of file diff --git a/AMREZ.EOP.Infrastructures/Repositories/TenantRepository.cs b/AMREZ.EOP.Infrastructures/Repositories/TenantRepository.cs index 781b06a..6876e79 100644 --- a/AMREZ.EOP.Infrastructures/Repositories/TenantRepository.cs +++ b/AMREZ.EOP.Infrastructures/Repositories/TenantRepository.cs @@ -17,12 +17,17 @@ public sealed class TenantRepository : ITenantRepository private readonly IHttpContextAccessor _http; public TenantRepository(IDbScope scope, ITenantResolver resolver, IHttpContextAccessor http) - { _scope = scope; _resolver = resolver; _http = http; } + { + _scope = scope; + _resolver = resolver; + _http = http; + } private AppDbContext Db() { var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext"); - var platform = _resolver.Resolve(http, "@platform") ?? throw new InvalidOperationException("No platform tenant"); + var platform = _resolver.Resolve(http, "@platform") ?? + throw new InvalidOperationException("No platform tenant"); _scope.EnsureForTenant(platform); return _scope.Get(); } @@ -55,7 +60,8 @@ public sealed class TenantRepository : ITenantRepository return await Db().SaveChangesAsync(ct) > 0; } - public async Task UpdateAsync(TenantConfig row, DateTimeOffset? ifUnmodifiedSince, CancellationToken ct = default) + public async Task UpdateAsync(TenantConfig row, DateTimeOffset? ifUnmodifiedSince, + CancellationToken ct = default) { var key = Norm(row.TenantKey); var db = Db(); @@ -65,8 +71,12 @@ public sealed class TenantRepository : ITenantRepository if (ifUnmodifiedSince.HasValue && cur.UpdatedAtUtc > ifUnmodifiedSince.Value) return false; - cur.Schema = row.Schema is null ? cur.Schema : (string.IsNullOrWhiteSpace(row.Schema) ? null : row.Schema.Trim()); - cur.ConnectionString = row.ConnectionString is null ? cur.ConnectionString : (string.IsNullOrWhiteSpace(row.ConnectionString) ? null : row.ConnectionString.Trim()); + cur.Schema = row.Schema is null + ? cur.Schema + : (string.IsNullOrWhiteSpace(row.Schema) ? null : row.Schema.Trim()); + cur.ConnectionString = row.ConnectionString is null + ? cur.ConnectionString + : (string.IsNullOrWhiteSpace(row.ConnectionString) ? null : row.ConnectionString.Trim()); cur.Mode = row.Mode; cur.IsActive = row.IsActive; cur.UpdatedAtUtc = DateTimeOffset.UtcNow; @@ -112,7 +122,13 @@ public sealed class TenantRepository : ITenantRepository var db = Db(); var ex = await db.Set().FirstOrDefaultAsync(x => x.Domain == d, ct); if (ex is null) - await db.Set().AddAsync(new TenantDomain { Domain = d, TenantKey = key, IsPlatformBaseDomain = false, IsActive = true, UpdatedAtUtc = DateTimeOffset.UtcNow }, ct); + await db.Set() + .AddAsync( + new TenantDomain + { + Domain = d, TenantKey = key, IsPlatformBaseDomain = false, IsActive = true, + UpdatedAtUtc = DateTimeOffset.UtcNow + }, ct); else { ex.TenantKey = key; @@ -120,6 +136,7 @@ public sealed class TenantRepository : ITenantRepository ex.IsActive = true; ex.UpdatedAtUtc = DateTimeOffset.UtcNow; } + return await db.SaveChangesAsync(ct) > 0; } @@ -147,6 +164,7 @@ public sealed class TenantRepository : ITenantRepository var key = Norm(tenantKey!); q = q.Where(x => x.TenantKey == key || x.IsPlatformBaseDomain); } + return await q.OrderBy(x => x.Domain).ToListAsync(ct); } @@ -156,7 +174,13 @@ public sealed class TenantRepository : ITenantRepository var db = Db(); var ex = await db.Set().FirstOrDefaultAsync(x => x.Domain == d, ct); if (ex is null) - await db.Set().AddAsync(new TenantDomain { Domain = d, TenantKey = null, IsPlatformBaseDomain = true, IsActive = true, UpdatedAtUtc = DateTimeOffset.UtcNow }, ct); + await db.Set() + .AddAsync( + new TenantDomain + { + Domain = d, TenantKey = null, IsPlatformBaseDomain = true, IsActive = true, + UpdatedAtUtc = DateTimeOffset.UtcNow + }, ct); else { ex.TenantKey = null; @@ -164,6 +188,7 @@ public sealed class TenantRepository : ITenantRepository ex.IsActive = true; ex.UpdatedAtUtc = DateTimeOffset.UtcNow; } + return await db.SaveChangesAsync(ct) > 0; } @@ -180,4 +205,36 @@ public sealed class TenantRepository : ITenantRepository return await db.SaveChangesAsync(ct) > 0; } + + /// + /// คืนค่า TenantKey จาก TenantId (Guid) ถ้าไม่เจอหรือไม่ active คืน null + /// + public async Task GetTenantKeyByTenantIdAsync(Guid tenantId, CancellationToken ct = default) + { + var key = await Db().Set() + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.IsActive) + .Select(x => x.TenantKey) + .FirstOrDefaultAsync(ct); + + return string.IsNullOrWhiteSpace(key) ? null : key; + } + + /// + /// คืนค่าแม็พ TenantId -> TenantKey เฉพาะที่เจอและ active + /// + public async Task> MapTenantKeysAsync(IEnumerable tenantIds, CancellationToken ct = default) + { + var ids = (tenantIds ?? Array.Empty()).Distinct().ToArray(); + if (ids.Length == 0) return new Dictionary(); + + var rows = await Db().Set() + .AsNoTracking() + .Where(x => ids.Contains(x.TenantId) && x.IsActive) + .Select(x => new { x.TenantId, x.TenantKey }) + .ToListAsync(ct); + + return rows.ToDictionary(x => x.TenantId, x => x.TenantKey); + } + } \ No newline at end of file diff --git a/AMREZ.EOP.Infrastructures/Repositories/UserRepository.cs b/AMREZ.EOP.Infrastructures/Repositories/UserRepository.cs index 85f259e..deed518 100644 --- a/AMREZ.EOP.Infrastructures/Repositories/UserRepository.cs +++ b/AMREZ.EOP.Infrastructures/Repositories/UserRepository.cs @@ -65,36 +65,33 @@ public class UserRepository : IUserRepository public async Task FindActiveByEmailAsync(string email, CancellationToken ct = default) { - var tid = TenantId(); - var tidStr = tid.ToString(); var r = _redis?.GetDatabase(); + var norm = email.Trim().ToLowerInvariant(); if (r is not null) { try { - var cached = await r.StringGetAsync(IdentityEmailKey(tidStr, email)); + var cached = await r.StringGetAsync($"uidx:{norm}"); if (cached.HasValue) { var hit = JsonSerializer.Deserialize(cached!); if (hit?.IsActive == true) return hit; } } - catch { } + catch { /* ignore cache errors */ } } var db = _scope.Get(); - var norm = email.Trim().ToLowerInvariant(); - var user = await db.Users - .AsNoTracking() - .Where(u => u.TenantId == tid && u.IsActive) - .Where(u => db.UserIdentities.Any(i => - i.TenantId == tid && - i.UserId == u.Id && - i.Type == IdentityType.Email && - i.Identifier == norm)) - .FirstOrDefaultAsync(ct); + var user = await ( + from u in db.Users.AsNoTracking() + join i in db.UserIdentities.AsNoTracking() on u.Id equals i.UserId + where u.IsActive + && i.Type == IdentityType.Email + && i.Identifier == norm + select u + ).FirstOrDefaultAsync(ct); if (user is not null && r is not null) { @@ -102,10 +99,10 @@ public class UserRepository : IUserRepository { var payload = JsonSerializer.Serialize(user); var ttl = TimeSpan.FromMinutes(5); - await r.StringSetAsync(IdentityEmailKey(tidStr, norm), payload, ttl); - await r.StringSetAsync(UserIdKey(tidStr, user.Id), payload, ttl); + await r.StringSetAsync($"uidx:{norm}", payload, ttl); + await r.StringSetAsync($"user:{user.Id:N}", payload, ttl); } - catch { } + catch { /* ignore cache errors */ } } return user; @@ -291,9 +288,7 @@ public class UserRepository : IUserRepository public async Task CreateSessionAsync(UserSession session, CancellationToken ct = default) { - var tid = TenantId(); var db = _scope.Get(); - session.TenantId = tid; await db.UserSessions.AddAsync(session, ct); await db.SaveChangesAsync(ct); return session; @@ -383,11 +378,10 @@ public class UserRepository : IUserRepository public async Task GetUserSecurityStampAsync(Guid userId, CancellationToken ct = default) { - var tid = TenantId(); var db = _scope.Get(); return await db.Users .AsNoTracking() - .Where(x => x.TenantId == tid && x.Id == userId) + .Where(x => x.Id == userId) .Select(x => x.SecurityStamp) .FirstOrDefaultAsync(ct); } diff --git a/AMREZ.EOP.Infrastructures/Security/JwtFactory.cs b/AMREZ.EOP.Infrastructures/Security/JwtFactory.cs index 9fc29ad..b90442a 100644 --- a/AMREZ.EOP.Infrastructures/Security/JwtFactory.cs +++ b/AMREZ.EOP.Infrastructures/Security/JwtFactory.cs @@ -28,23 +28,23 @@ public sealed class JwtFactory : IJwtFactory public (string token, DateTimeOffset expiresAt) CreateAccessToken(IEnumerable claims) { - var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext"); - var tenant = _resolver.Resolve(http) ?? throw new InvalidOperationException("No tenant context"); + var tenantId = claims.FirstOrDefault(c => c.Type == "tenant_id")?.Value + ?? throw new InvalidOperationException("tenant_id claim missing"); - var material = !string.IsNullOrWhiteSpace(tenant.Id) ? tenant.Id! : tenant.TenantKey!; - var keyBytes = SHA256.HashData(Encoding.UTF8.GetBytes(material)); - var cred = new SigningCredentials(new SymmetricSecurityKey(keyBytes), SecurityAlgorithms.HmacSha256); + var keyBytes = SHA256.HashData(Encoding.UTF8.GetBytes(tenantId)); + var creds = new SigningCredentials(new SymmetricSecurityKey(keyBytes), SecurityAlgorithms.HmacSha256); var now = DateTimeOffset.UtcNow; var exp = now.AddMinutes(AccessMinutes); var jwt = new JwtSecurityToken( - issuer: Issuer, + issuer: Issuer, audience: Audience, claims: claims, notBefore: now.UtcDateTime, expires: exp.UtcDateTime, - signingCredentials: cred); + signingCredentials: creds + ); return (new JwtSecurityTokenHandler().WriteToken(jwt), exp); } diff --git a/AMREZ.EOP.Infrastructures/Tenancy/DefaultTenantResolver.cs b/AMREZ.EOP.Infrastructures/Tenancy/DefaultTenantResolver.cs index d651d9b..0d61da2 100644 --- a/AMREZ.EOP.Infrastructures/Tenancy/DefaultTenantResolver.cs +++ b/AMREZ.EOP.Infrastructures/Tenancy/DefaultTenantResolver.cs @@ -11,7 +11,6 @@ public sealed class DefaultTenantResolver : ITenantResolver private readonly TenantMap _map; private readonly ILogger? _log; - // แพลตฟอร์มสลัก (ปรับตามระบบจริงได้) — ใช้ "public" เป็นค่าเริ่ม private const string PlatformSlug = "public"; public DefaultTenantResolver( @@ -75,14 +74,7 @@ public sealed class DefaultTenantResolver : ITenantResolver _log?.LogInformation("Resolved by subdomain: {Sub} cid={Cid}", sub, cid); return bySub; } - - if (hint is LoginRequest body && !string.IsNullOrWhiteSpace(body.Tenant) && - _map.TryGetBySlug(body.Tenant!, out var byBody)) - { - _log?.LogInformation("Resolved by body: {Tenant} cid={Cid}", body.Tenant, cid); - return byBody; - } - + _log?.LogWarning("Resolve FAILED host={Host} header={Header} cid={Cid}", host, string.IsNullOrWhiteSpace(header) ? "" : header, cid); return null;