🐛 Dozen of bugs fixes
This commit is contained in:
		| @@ -9,6 +9,7 @@ using Microsoft.Extensions.Options; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Controllers; | ||||
|  | ||||
| @@ -110,7 +111,7 @@ public class OidcProviderController( | ||||
|         return Ok(userInfo); | ||||
|     } | ||||
|  | ||||
|     [HttpGet(".well-known/openid-configuration")] | ||||
|     [HttpGet("/.well-known/openid-configuration")] | ||||
|     public IActionResult GetConfiguration() | ||||
|     { | ||||
|         var baseUrl = configuration["BaseUrl"]; | ||||
| @@ -119,10 +120,10 @@ public class OidcProviderController( | ||||
|         return Ok(new | ||||
|         { | ||||
|             issuer = issuer, | ||||
|             authorization_endpoint = $"{baseUrl}/connect/authorize", | ||||
|             authorization_endpoint = $"{baseUrl}/auth/authorize", | ||||
|             token_endpoint = $"{baseUrl}/auth/open/token", | ||||
|             userinfo_endpoint = $"{baseUrl}/auth/open/userinfo", | ||||
|             jwks_uri = $"{baseUrl}/.well-known/openid-configuration/jwks", | ||||
|             jwks_uri = $"{baseUrl}/.well-known/jwks", | ||||
|             scopes_supported = new[] { "openid", "profile", "email" }, | ||||
|             response_types_supported = new[] | ||||
|                 { "code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token" }, | ||||
| @@ -139,11 +140,17 @@ public class OidcProviderController( | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("jwks")] | ||||
|     [HttpGet("/.well-known/jwks")] | ||||
|     public IActionResult GetJwks() | ||||
|     { | ||||
|         var keyBytes = Encoding.UTF8.GetBytes(options.Value.SigningKey); | ||||
|         var keyId = Convert.ToBase64String(SHA256.HashData(keyBytes)[..8]) | ||||
|         using var rsa = options.Value.GetRsaPublicKey(); | ||||
|         if (rsa == null) | ||||
|         { | ||||
|             return BadRequest("Public key is not configured"); | ||||
|         } | ||||
|  | ||||
|         var parameters = rsa.ExportParameters(false); | ||||
|         var keyId = Convert.ToBase64String(SHA256.HashData(parameters.Modulus!)[..8]) | ||||
|             .Replace("+", "-") | ||||
|             .Replace("/", "_") | ||||
|             .Replace("=", ""); | ||||
| @@ -154,11 +161,12 @@ public class OidcProviderController( | ||||
|             { | ||||
|                 new | ||||
|                 { | ||||
|                     kty = "oct", | ||||
|                     kty = "RSA", | ||||
|                     use = "sig", | ||||
|                     kid = keyId, | ||||
|                     k = Convert.ToBase64String(keyBytes), | ||||
|                     alg = "HS256" | ||||
|                     n = Base64UrlEncoder.Encode(parameters.Modulus!), | ||||
|                     e = Base64UrlEncoder.Encode(parameters.Exponent!), | ||||
|                     alg = "RS256" | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|   | ||||
| @@ -1,13 +1,36 @@ | ||||
| using System; | ||||
| using System.Security.Cryptography; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Options; | ||||
|  | ||||
| public class OidcProviderOptions | ||||
| { | ||||
|     public string IssuerUri { get; set; } = "https://your-issuer-uri.com"; | ||||
|     public string SigningKey { get; set; } = "replace-with-a-secure-random-key"; | ||||
|     public string? PublicKeyPath { get; set; } | ||||
|     public string? PrivateKeyPath { get; set; } | ||||
|     public TimeSpan AccessTokenLifetime { get; set; } = TimeSpan.FromHours(1); | ||||
|     public TimeSpan RefreshTokenLifetime { get; set; } = TimeSpan.FromDays(30); | ||||
|     public TimeSpan AuthorizationCodeLifetime { get; set; } = TimeSpan.FromMinutes(5); | ||||
|     public bool RequireHttpsMetadata { get; set; } = true; | ||||
| } | ||||
|  | ||||
|     public RSA? GetRsaPrivateKey() | ||||
|     { | ||||
|         if (string.IsNullOrEmpty(PrivateKeyPath) || !File.Exists(PrivateKeyPath)) | ||||
|             return null; | ||||
|  | ||||
|         var privateKey = File.ReadAllText(PrivateKeyPath); | ||||
|         var rsa = RSA.Create(); | ||||
|         rsa.ImportFromPem(privateKey.AsSpan()); | ||||
|         return rsa; | ||||
|     } | ||||
|  | ||||
|     public RSA? GetRsaPublicKey() | ||||
|     { | ||||
|         if (string.IsNullOrEmpty(PublicKeyPath) || !File.Exists(PublicKeyPath)) | ||||
|             return null; | ||||
|  | ||||
|         var publicKey = File.ReadAllText(PublicKeyPath); | ||||
|         var rsa = RSA.Create(); | ||||
|         rsa.ImportFromPem(publicKey.AsSpan()); | ||||
|         return rsa; | ||||
|     } | ||||
| } | ||||
| @@ -75,7 +75,7 @@ public class OidcProviderService( | ||||
|  | ||||
|         // Generate access token | ||||
|         var accessToken = GenerateJwtToken(client, session, expiresAt, scopes); | ||||
|         var refreshToken = GenerateRefreshToken(); | ||||
|         var refreshToken = GenerateRefreshToken(session); | ||||
|  | ||||
|         // In a real implementation, you would store the token in the database | ||||
|         // For this example, we'll just return the token without storing it | ||||
| @@ -92,35 +92,38 @@ public class OidcProviderService( | ||||
|     } | ||||
|  | ||||
|     private string GenerateJwtToken( | ||||
|         CustomApp client, | ||||
|         Session session, | ||||
|         Instant expiresAt, | ||||
|         IEnumerable<string>? scopes = null | ||||
|     CustomApp client, | ||||
|     Session session, | ||||
|     Instant expiresAt, | ||||
|     IEnumerable<string>? scopes = null | ||||
|     ) | ||||
|     { | ||||
|         var tokenHandler = new JwtSecurityTokenHandler(); | ||||
|         var key = Encoding.ASCII.GetBytes(_options.SigningKey); | ||||
|  | ||||
|         var clock = SystemClock.Instance; | ||||
|         var now = clock.GetCurrentInstant(); | ||||
|  | ||||
|         var tokenDescriptor = new SecurityTokenDescriptor | ||||
|         { | ||||
|             Subject = new ClaimsIdentity([ | ||||
|                 new Claim(JwtRegisteredClaimNames.Sub, session.Id.ToString()), | ||||
|                 new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), | ||||
|                 new Claim(JwtRegisteredClaimNames.Iat, clock.GetCurrentInstant().ToUnixTimeSeconds().ToString(), | ||||
|                     ClaimValueTypes.Integer64), | ||||
|                 new Claim("client_id", client.Id.ToString()) | ||||
|             new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), | ||||
|             new Claim(JwtRegisteredClaimNames.Iat, now.ToUnixTimeSeconds().ToString(), | ||||
|                 ClaimValueTypes.Integer64), | ||||
|             new Claim("client_id", client.Id.ToString()) | ||||
|             ]), | ||||
|             Expires = expiresAt.ToDateTimeUtc(), | ||||
|             Issuer = _options.IssuerUri, | ||||
|             Audience = client.Id.ToString(), | ||||
|             SigningCredentials = new SigningCredentials( | ||||
|                 new SymmetricSecurityKey(key), | ||||
|                 SecurityAlgorithms.HmacSha256Signature | ||||
|             ) | ||||
|             Audience = client.Id.ToString() | ||||
|         }; | ||||
|  | ||||
|         // Add scopes as claims if provided, otherwise use client's default scopes | ||||
|         // Try to use RSA signing if keys are available, fall back to HMAC | ||||
|         var rsaPrivateKey = _options.GetRsaPrivateKey(); | ||||
|         tokenDescriptor.SigningCredentials = new SigningCredentials( | ||||
|             new RsaSecurityKey(rsaPrivateKey), | ||||
|             SecurityAlgorithms.RsaSha256 | ||||
|         ); | ||||
|  | ||||
|         // Add scopes as claims if provided | ||||
|         var effectiveScopes = scopes?.ToList() ?? client.AllowedScopes?.ToList() ?? new List<string>(); | ||||
|         if (effectiveScopes.Any()) | ||||
|         { | ||||
| @@ -132,15 +135,40 @@ public class OidcProviderService( | ||||
|         return tokenHandler.WriteToken(token); | ||||
|     } | ||||
|  | ||||
|     private static string GenerateRefreshToken() | ||||
|     public (bool isValid, JwtSecurityToken? token) ValidateToken(string token) | ||||
|     { | ||||
|         using var rng = RandomNumberGenerator.Create(); | ||||
|         var bytes = new byte[32]; | ||||
|         rng.GetBytes(bytes); | ||||
|         return Convert.ToBase64String(bytes) | ||||
|             .Replace("+", "-") | ||||
|             .Replace("/", "_") | ||||
|             .Replace("=", ""); | ||||
|         try | ||||
|         { | ||||
|             var tokenHandler = new JwtSecurityTokenHandler(); | ||||
|             var validationParameters = new TokenValidationParameters | ||||
|             { | ||||
|                 ValidateIssuer = true, | ||||
|                 ValidIssuer = _options.IssuerUri, | ||||
|                 ValidateAudience = true, | ||||
|                 ValidateLifetime = true, | ||||
|                 ClockSkew = TimeSpan.Zero | ||||
|             }; | ||||
|  | ||||
|             // Try to use RSA validation if public key is available | ||||
|             var rsaPublicKey = _options.GetRsaPublicKey(); | ||||
|             validationParameters.IssuerSigningKey = new RsaSecurityKey(rsaPublicKey); | ||||
|             validationParameters.ValidateIssuerSigningKey = true; | ||||
|             validationParameters.ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 }; | ||||
|  | ||||
|  | ||||
|             tokenHandler.ValidateToken(token, validationParameters, out var validatedToken); | ||||
|             return (true, (JwtSecurityToken)validatedToken); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "Token validation failed"); | ||||
|             return (false, null); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static string GenerateRefreshToken(Session session) | ||||
|     { | ||||
|         return Convert.ToBase64String(Encoding.UTF8.GetBytes(session.Id.ToString())); | ||||
|     } | ||||
|  | ||||
|     private static bool VerifyHashedSecret(string secret, string hashedSecret) | ||||
| @@ -150,35 +178,6 @@ public class OidcProviderService( | ||||
|         return string.Equals(secret, hashedSecret, StringComparison.Ordinal); | ||||
|     } | ||||
|  | ||||
|     public JwtSecurityToken? ValidateToken(string token) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             var tokenHandler = new JwtSecurityTokenHandler(); | ||||
|             var key = Encoding.ASCII.GetBytes(_options.SigningKey); | ||||
|  | ||||
|             tokenHandler.ValidateToken(token, new TokenValidationParameters | ||||
|             { | ||||
|                 ValidateIssuerSigningKey = true, | ||||
|                 IssuerSigningKey = new SymmetricSecurityKey(key), | ||||
|                 ValidateIssuer = true, | ||||
|                 ValidIssuer = _options.IssuerUri, | ||||
|                 ValidateAudience = true, | ||||
|                 ValidateLifetime = true, | ||||
|                 ClockSkew = TimeSpan.Zero | ||||
|             }, out var validatedToken); | ||||
|  | ||||
|             return (JwtSecurityToken)validatedToken; | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "Token validation failed"); | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Authorization codes are now managed through ICacheService | ||||
|  | ||||
|     public async Task<string> GenerateAuthorizationCodeAsync( | ||||
|         Guid clientId, | ||||
|         Guid userId, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user