🐛 Dozen of bugs fixes
This commit is contained in:
@ -1,12 +1,19 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Encodings.Web;
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Sphere.Auth.OidcProvider.Options;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Sphere.Storage.Handlers;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using NodaTime;
|
||||
using System.Text;
|
||||
using DysonNetwork.Sphere.Auth.OidcProvider.Controllers;
|
||||
using DysonNetwork.Sphere.Auth.OidcProvider.Services;
|
||||
using SystemClock = NodaTime.SystemClock;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
@ -22,6 +29,7 @@ public enum TokenType
|
||||
{
|
||||
AuthKey,
|
||||
ApiKey,
|
||||
OidcKey,
|
||||
Unknown
|
||||
}
|
||||
|
||||
@ -39,6 +47,7 @@ public class DysonTokenAuthHandler(
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
AppDatabase database,
|
||||
OidcProviderService oidc,
|
||||
ICacheService cache,
|
||||
FlushBufferService fbs
|
||||
)
|
||||
@ -142,35 +151,60 @@ public class DysonTokenAuthHandler(
|
||||
|
||||
try
|
||||
{
|
||||
// Split the token
|
||||
var parts = token.Split('.');
|
||||
if (parts.Length != 2)
|
||||
return false;
|
||||
|
||||
// Decode the payload
|
||||
var payloadBytes = Base64UrlDecode(parts[0]);
|
||||
switch (parts.Length)
|
||||
{
|
||||
// Handle JWT tokens (3 parts)
|
||||
case 3:
|
||||
{
|
||||
var (isValid, jwtResult) = oidc.ValidateToken(token);
|
||||
if (!isValid) return false;
|
||||
var jti = jwtResult?.Claims.FirstOrDefault(c => c.Type == "jti")?.Value;
|
||||
if (jti is null) return false;
|
||||
|
||||
// Extract session ID
|
||||
sessionId = new Guid(payloadBytes);
|
||||
return Guid.TryParse(jti, out sessionId);
|
||||
}
|
||||
// Handle compact tokens (2 parts)
|
||||
case 2:
|
||||
// Original compact token validation logic
|
||||
try
|
||||
{
|
||||
// Decode the payload
|
||||
var payloadBytes = Base64UrlDecode(parts[0]);
|
||||
|
||||
// Load public key for verification
|
||||
var publicKeyPem = File.ReadAllText(configuration["Jwt:PublicKeyPath"]!);
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(publicKeyPem);
|
||||
// Extract session ID
|
||||
sessionId = new Guid(payloadBytes);
|
||||
|
||||
// Verify signature
|
||||
var signature = Base64UrlDecode(parts[1]);
|
||||
return rsa.VerifyData(payloadBytes, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
// Load public key for verification
|
||||
var publicKeyPem = File.ReadAllText(configuration["AuthToken:PublicKeyPath"]!);
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(publicKeyPem);
|
||||
|
||||
// Verify signature
|
||||
var signature = Base64UrlDecode(parts[1]);
|
||||
return rsa.VerifyData(payloadBytes, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Token validation failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] Base64UrlDecode(string base64Url)
|
||||
{
|
||||
string padded = base64Url
|
||||
var padded = base64Url
|
||||
.Replace('-', '+')
|
||||
.Replace('_', '/');
|
||||
|
||||
@ -195,20 +229,23 @@ public class DysonTokenAuthHandler(
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Check for token in Authorization header
|
||||
var authHeader = request.Headers.Authorization.ToString();
|
||||
if (!string.IsNullOrEmpty(authHeader))
|
||||
{
|
||||
if (authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var token = authHeader["Bearer ".Length..].Trim();
|
||||
var parts = token.Split('.');
|
||||
|
||||
return new TokenInfo
|
||||
{
|
||||
Token = authHeader["Bearer ".Length..].Trim(),
|
||||
Type = TokenType.AuthKey
|
||||
Token = token,
|
||||
Type = parts.Length == 3 ? TokenType.OidcKey : TokenType.AuthKey
|
||||
};
|
||||
}
|
||||
|
||||
if (authHeader.StartsWith("AtField ", StringComparison.OrdinalIgnoreCase))
|
||||
else if (authHeader.StartsWith("AtField ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new TokenInfo
|
||||
{
|
||||
@ -216,8 +253,7 @@ public class DysonTokenAuthHandler(
|
||||
Type = TokenType.AuthKey
|
||||
};
|
||||
}
|
||||
|
||||
if (authHeader.StartsWith("AkField ", StringComparison.OrdinalIgnoreCase))
|
||||
else if (authHeader.StartsWith("AkField ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new TokenInfo
|
||||
{
|
||||
@ -233,10 +269,11 @@ public class DysonTokenAuthHandler(
|
||||
return new TokenInfo
|
||||
{
|
||||
Token = cookieToken,
|
||||
Type = TokenType.AuthKey
|
||||
Type = cookieToken.Count(c => c == '.') == 2 ? TokenType.OidcKey : TokenType.AuthKey
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -156,7 +156,7 @@ public class AuthService(
|
||||
public string CreateToken(Session session)
|
||||
{
|
||||
// Load the private key for signing
|
||||
var privateKeyPem = File.ReadAllText(config["Jwt:PrivateKeyPath"]!);
|
||||
var privateKeyPem = File.ReadAllText(config["AuthToken:PrivateKeyPath"]!);
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(privateKeyPem);
|
||||
|
||||
@ -263,7 +263,7 @@ public class AuthService(
|
||||
sessionId = new Guid(payloadBytes);
|
||||
|
||||
// Load public key for verification
|
||||
var publicKeyPem = File.ReadAllText(config["Jwt:PublicKeyPath"]!);
|
||||
var publicKeyPem = File.ReadAllText(config["AuthToken:PublicKeyPath"]!);
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(publicKeyPem);
|
||||
|
||||
|
@ -4,8 +4,8 @@ namespace DysonNetwork.Sphere.Auth;
|
||||
|
||||
public class CompactTokenService(IConfiguration config)
|
||||
{
|
||||
private readonly string _privateKeyPath = config["Jwt:PrivateKeyPath"]
|
||||
?? throw new InvalidOperationException("Jwt:PrivateKeyPath configuration is missing");
|
||||
private readonly string _privateKeyPath = config["AuthToken:PrivateKeyPath"]
|
||||
?? throw new InvalidOperationException("AuthToken:PrivateKeyPath configuration is missing");
|
||||
|
||||
public string CreateToken(Session session)
|
||||
{
|
||||
@ -54,7 +54,7 @@ public class CompactTokenService(IConfiguration config)
|
||||
sessionId = new Guid(payloadBytes);
|
||||
|
||||
// Load public key for verification
|
||||
var publicKeyPem = File.ReadAllText(config["Jwt:PublicKeyPath"]!);
|
||||
var publicKeyPem = File.ReadAllText(config["AuthToken:PublicKeyPath"]!);
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(publicKeyPem);
|
||||
|
||||
|
@ -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