using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using DorkNet.Server.Auth; using DorkNet.Server.Data; namespace DorkNet.Server.Services; public class AuthService(IConfiguration config, DorkNetDbContext db, DomainConfig domain, IdentityServerSigningKeyProvider signingKeyProvider) { public const string AccessCookieName = "dorknet_access_token"; public const string LegacyIssuer = "https://api.rec.net/"; // Resolution order MUST match Program.cs's JwtBearer validator setup — // env var first, config key second — otherwise tokens are signed with // one secret and validated against another and every authenticated // request 401s. In production we set DORKNET_JWT_SECRET in Coolify // (RECNET_JWT_SECRET kept as fallback for backward compat with older // configs) and leave Jwt:Secret as the appsettings.json placeholder; // the mismatched-secret bug shows up only there. private string SecretKey => Environment.GetEnvironmentVariable("DORKNET_JWT_SECRET") ?? Environment.GetEnvironmentVariable("RECNET_JWT_SECRET") ?? config["Jwt:Secret"] ?? throw new InvalidOperationException( "JWT secret not configured. Set DORKNET_JWT_SECRET env var or " + "Jwt:Secret in appsettings.Local.json."); public string GenerateToken(long playerId) => CreateJwt(playerId, TimeSpan.FromHours(12), "access"); /// /// Issues an (access, refresh) pair for the OAuth-style password-grant /// response shape (Login.LoginResponse). Access tokens are short-lived; /// the refresh token is what api/platformlogin/refresh accepts later. /// public (string AccessToken, string RefreshToken) GenerateTokenPair(long playerId) => (CreateJwt(playerId, TimeSpan.FromHours(12), "access"), CreateJwt(playerId, TimeSpan.FromDays(30), "refresh")); private string CreateJwt(long playerId, TimeSpan lifetime, string tokenType) { var handler = new JwtSecurityTokenHandler(); var claims = RecRoomIdentityClaims.CreateAsync(db, playerId) .GetAwaiter() .GetResult(); claims.Add(new Claim("token_type", tokenType)); var descriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = DateTime.UtcNow.Add(lifetime), Issuer = domain.AuthIssuer, SigningCredentials = new SigningCredentials(signingKeyProvider.SigningKey, SecurityAlgorithms.RsaSha256), }; return handler.WriteToken(handler.CreateJwtSecurityToken(descriptor)); } public long? ValidateToken(string token) { try { var handler = new JwtSecurityTokenHandler(); var principal = handler.ValidateToken(token, new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKeys = signingKeyProvider.ValidationKeys, ValidateIssuer = true, ValidIssuers = new[] { domain.AuthIssuer, LegacyIssuer }, ValidateAudience = false, ClockSkew = TimeSpan.Zero, }, out _); var idClaim = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value ?? principal.FindFirst("sub")?.Value ?? principal.FindFirst("accountId")?.Value ?? principal.FindFirst("account_id")?.Value ?? principal.FindFirst("accountid")?.Value; return idClaim is not null ? long.Parse(idClaim) : null; } catch { return null; } } }