💥 The newly crafted Dyson Token
This commit is contained in:
		| @@ -11,7 +11,7 @@ public class AccountService( | ||||
|     ICacheService cache | ||||
| ) | ||||
| { | ||||
|     public const string AccountCachePrefix = "Account_"; | ||||
|     public const string AccountCachePrefix = "account:"; | ||||
|  | ||||
|     public async Task PurgeAccountCache(Account account) | ||||
|     { | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| 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.Storage; | ||||
| using Microsoft.AspNetCore.Authentication; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Options; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| using SystemClock = NodaTime.SystemClock; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Auth; | ||||
|  | ||||
| @@ -13,44 +16,104 @@ public static class AuthConstants | ||||
|     public const string TokenQueryParamName = "tk"; | ||||
| } | ||||
|  | ||||
| public enum TokenType | ||||
| { | ||||
|     AuthKey, | ||||
|     ApiKey, | ||||
|     Unknown | ||||
| } | ||||
|  | ||||
| public class TokenInfo | ||||
| { | ||||
|     public string Token { get; set; } = string.Empty; | ||||
|     public TokenType Type { get; set; } = TokenType.Unknown; | ||||
| } | ||||
|  | ||||
| public class DysonTokenAuthOptions : AuthenticationSchemeOptions; | ||||
|  | ||||
| public class DysonTokenAuthHandler : AuthenticationHandler<DysonTokenAuthOptions> | ||||
| public class DysonTokenAuthHandler( | ||||
|     IOptionsMonitor<DysonTokenAuthOptions> options, | ||||
|     IConfiguration configuration, | ||||
|     ILoggerFactory logger, | ||||
|     UrlEncoder encoder, | ||||
|     AppDatabase database, | ||||
|     ICacheService cache | ||||
| ) | ||||
|     : AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder) | ||||
| { | ||||
|     private TokenValidationParameters _tokenValidationParameters; | ||||
|     private const string AuthCachePrefix = "auth:"; | ||||
|      | ||||
|     public DysonTokenAuthHandler( | ||||
|         IOptionsMonitor<DysonTokenAuthOptions> options, | ||||
|         IConfiguration configuration, | ||||
|         ILoggerFactory logger, | ||||
|         UrlEncoder encoder) | ||||
|         : base(options, logger, encoder) | ||||
|     { | ||||
|         var publicKey = File.ReadAllText(configuration["Jwt:PublicKeyPath"]!); | ||||
|         var rsa = RSA.Create(); | ||||
|         rsa.ImportFromPem(publicKey); | ||||
|         _tokenValidationParameters = new TokenValidationParameters | ||||
|         { | ||||
|             ValidateIssuer = true, | ||||
|             ValidateAudience = false, | ||||
|             ValidateLifetime = true, | ||||
|             ValidateIssuerSigningKey = true, | ||||
|             ValidIssuer = "solar-network", | ||||
|             IssuerSigningKey = new RsaSecurityKey(rsa) | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     protected override async Task<AuthenticateResult> HandleAuthenticateAsync() | ||||
|     { | ||||
|         var token = _ExtractToken(Request); | ||||
|         var tokenInfo = _ExtractToken(Request); | ||||
|  | ||||
|         if (string.IsNullOrEmpty(token)) | ||||
|         if (tokenInfo == null || string.IsNullOrEmpty(tokenInfo.Token)) | ||||
|             return AuthenticateResult.Fail("No token was provided."); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var tokenHandler = new JwtSecurityTokenHandler(); | ||||
|             var principal = tokenHandler.ValidateToken(token, _tokenValidationParameters, out var validatedToken); | ||||
|             var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|  | ||||
|             // Validate token and extract session ID | ||||
|             if (!ValidateToken(tokenInfo.Token, out var sessionId)) | ||||
|                 return AuthenticateResult.Fail("Invalid token."); | ||||
|  | ||||
|             // Try to get session from cache first | ||||
|             var session = await cache.GetAsync<Session>($"{AuthCachePrefix}{sessionId}"); | ||||
|  | ||||
|             // If not in cache, load from database | ||||
|             if (session is null) | ||||
|             { | ||||
|                 session = await database.AuthSessions | ||||
|                     .Where(e => e.Id == sessionId) | ||||
|                     .Include(e => e.Challenge) | ||||
|                     .Include(e => e.Account) | ||||
|                     .ThenInclude(e => e.Profile) | ||||
|                     .FirstOrDefaultAsync(); | ||||
|  | ||||
|                 if (session is not null) | ||||
|                 { | ||||
|                     // Store in cache for future requests | ||||
|                     await cache.SetWithGroupsAsync( | ||||
|                         $"Auth_{sessionId}", | ||||
|                         session, | ||||
|                         [$"{AccountService.AccountCachePrefix}{session.Account.Id}"], | ||||
|                         TimeSpan.FromHours(1) | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Check if the session exists | ||||
|             if (session == null) | ||||
|                 return AuthenticateResult.Fail("Session not found."); | ||||
|  | ||||
|             // Check if the session is expired | ||||
|             if (session.ExpiredAt.HasValue && session.ExpiredAt.Value < now) | ||||
|                 return AuthenticateResult.Fail("Session expired."); | ||||
|  | ||||
|             // Store user and session in the HttpContext.Items for easy access in controllers | ||||
|             Context.Items["CurrentUser"] = session.Account; | ||||
|             Context.Items["CurrentSession"] = session; | ||||
|             Context.Items["CurrentTokenType"] = tokenInfo.Type.ToString(); | ||||
|  | ||||
|             // Create claims from the session | ||||
|             var claims = new List<Claim> | ||||
|             { | ||||
|                 new("user_id", session.Account.Id.ToString()), | ||||
|                 new("session_id", session.Id.ToString()), | ||||
|                 new("token_type", tokenInfo.Type.ToString()) | ||||
|             }; | ||||
|  | ||||
|             // Add scopes as claims | ||||
|             session.Challenge.Scopes.ForEach(scope => claims.Add(new Claim("scope", scope))); | ||||
|  | ||||
|             // Add superuser claim if applicable | ||||
|             if (session.Account.IsSuperuser) | ||||
|                 claims.Add(new Claim("is_superuser", "1")); | ||||
|  | ||||
|             // Create the identity and principal | ||||
|             var identity = new ClaimsIdentity(claims, AuthConstants.SchemeName); | ||||
|             var principal = new ClaimsPrincipal(identity); | ||||
|  | ||||
|             var ticket = new AuthenticationTicket(principal, AuthConstants.SchemeName); | ||||
|             return AuthenticateResult.Success(ticket); | ||||
| @@ -61,17 +124,97 @@ public class DysonTokenAuthHandler : AuthenticationHandler<DysonTokenAuthOptions | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static string? _ExtractToken(HttpRequest request) | ||||
|     private bool ValidateToken(string token, out Guid sessionId) | ||||
|     { | ||||
|         sessionId = Guid.Empty; | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             // Split the token | ||||
|             var parts = token.Split('.'); | ||||
|             if (parts.Length != 2) | ||||
|                 return false; | ||||
|  | ||||
|             // Decode the payload | ||||
|             var payloadBytes = Base64UrlDecode(parts[0]); | ||||
|  | ||||
|             // Extract session ID | ||||
|             sessionId = new Guid(payloadBytes); | ||||
|  | ||||
|             // Load public key for verification | ||||
|             var publicKeyPem = File.ReadAllText(configuration["Jwt: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; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static byte[] Base64UrlDecode(string base64Url) | ||||
|     { | ||||
|         string padded = base64Url | ||||
|             .Replace('-', '+') | ||||
|             .Replace('_', '/'); | ||||
|  | ||||
|         switch (padded.Length % 4) | ||||
|         { | ||||
|             case 2: padded += "=="; break; | ||||
|             case 3: padded += "="; break; | ||||
|         } | ||||
|  | ||||
|         return Convert.FromBase64String(padded); | ||||
|     } | ||||
|  | ||||
|     private static TokenInfo? _ExtractToken(HttpRequest request) | ||||
|     { | ||||
|         // Check for token in query parameters | ||||
|         if (request.Query.TryGetValue(AuthConstants.TokenQueryParamName, out var queryToken)) | ||||
|             return queryToken; | ||||
|         { | ||||
|             return new TokenInfo | ||||
|             { | ||||
|                 Token = queryToken, | ||||
|                 Type = TokenType.AuthKey | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         // Check for token in Authorization header | ||||
|         var authHeader = request.Headers.Authorization.ToString(); | ||||
|         if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) | ||||
|             return authHeader["Bearer ".Length..].Trim(); | ||||
|         if (!string.IsNullOrEmpty(authHeader)) | ||||
|         { | ||||
|             if (authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 return new TokenInfo | ||||
|                 { | ||||
|                     Token = authHeader["Bearer ".Length..].Trim(), | ||||
|                     Type = TokenType.AuthKey | ||||
|                 }; | ||||
|             } | ||||
|             else if (authHeader.StartsWith("ApiKey ", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 return new TokenInfo | ||||
|                 { | ||||
|                     Token = authHeader["ApiKey ".Length..].Trim(), | ||||
|                     Type = TokenType.ApiKey | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return request.Cookies.TryGetValue(AuthConstants.TokenQueryParamName, out var cookieToken) | ||||
|             ? cookieToken | ||||
|             : null; | ||||
|         // Check for token in cookies | ||||
|         if (request.Cookies.TryGetValue(AuthConstants.TokenQueryParamName, out var cookieToken)) | ||||
|         { | ||||
|             return new TokenInfo | ||||
|             { | ||||
|                 Token = cookieToken, | ||||
|                 Type = TokenType.AuthKey | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
| @@ -182,8 +182,13 @@ public class AuthController( | ||||
|         public string? Code { get; set; } | ||||
|     } | ||||
|  | ||||
|     public class TokenExchangeResponse | ||||
|     { | ||||
|         public string Token { get; set; } = string.Empty; | ||||
|     } | ||||
|  | ||||
|     [HttpPost("token")] | ||||
|     public async Task<ActionResult<SignedTokenPair>> ExchangeToken([FromBody] TokenExchangeRequest request) | ||||
|     public async Task<ActionResult<TokenExchangeResponse>> ExchangeToken([FromBody] TokenExchangeRequest request) | ||||
|     { | ||||
|         Session? session; | ||||
|         switch (request.GrantType) | ||||
| @@ -218,26 +223,11 @@ public class AuthController( | ||||
|                 db.AuthSessions.Add(session); | ||||
|                 await db.SaveChangesAsync(); | ||||
|  | ||||
|                 return auth.CreateToken(session); | ||||
|                 var tk = auth.CreateToken(session); | ||||
|                 return Ok(new TokenExchangeResponse { Token = tk }); | ||||
|             case "refresh_token": | ||||
|                 var handler = new JwtSecurityTokenHandler(); | ||||
|                 var token = handler.ReadJwtToken(request.RefreshToken); | ||||
|                 var sessionIdClaim = token.Claims.FirstOrDefault(c => c.Type == "session_id")?.Value; | ||||
|  | ||||
|                 if (!Guid.TryParse(sessionIdClaim, out var sessionId)) | ||||
|                     return Unauthorized("Invalid or missing session_id claim in refresh token."); | ||||
|  | ||||
|                 session = await db.AuthSessions | ||||
|                     .Include(e => e.Account) | ||||
|                     .Include(e => e.Challenge) | ||||
|                     .FirstOrDefaultAsync(s => s.Id == sessionId); | ||||
|                 if (session is null) | ||||
|                     return NotFound("Session not found or expired."); | ||||
|  | ||||
|                 session.LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow); | ||||
|                 await db.SaveChangesAsync(); | ||||
|  | ||||
|                 return auth.CreateToken(session); | ||||
|                 // Since we no longer need the refresh token | ||||
|                 // This case is blank for now, thinking to mock it if the OIDC standard requires it | ||||
|             default: | ||||
|                 return BadRequest("Unsupported grant type."); | ||||
|         } | ||||
|   | ||||
| @@ -1,19 +1,8 @@ | ||||
| using System.IdentityModel.Tokens.Jwt; | ||||
| using System.Security.Claims; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text.Json; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Auth; | ||||
|  | ||||
| public class SignedTokenPair | ||||
| { | ||||
|     public string AccessToken { get; set; } = null!; | ||||
|     public string RefreshToken { get; set; } = null!; | ||||
|     public Instant ExpiredAt { get; set; } | ||||
| } | ||||
|  | ||||
| public class AuthService(IConfiguration config, IHttpClientFactory httpClientFactory) | ||||
| { | ||||
|     public async Task<bool> ValidateCaptcha(string token) | ||||
| @@ -69,47 +58,88 @@ public class AuthService(IConfiguration config, IHttpClientFactory httpClientFac | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public SignedTokenPair CreateToken(Session session) | ||||
|     public string CreateToken(Session session) | ||||
|     { | ||||
|         // Load the private key for signing | ||||
|         var privateKeyPem = File.ReadAllText(config["Jwt:PrivateKeyPath"]!); | ||||
|         var rsa = RSA.Create(); | ||||
|         using var rsa = RSA.Create(); | ||||
|         rsa.ImportFromPem(privateKeyPem); | ||||
|         var key = new RsaSecurityKey(rsa); | ||||
|          | ||||
|         // Create and return a single token | ||||
|         return CreateCompactToken(session.Id, rsa); | ||||
|     } | ||||
|  | ||||
|         var creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256); | ||||
|         var claims = new List<Claim> | ||||
|     private string CreateCompactToken(Guid sessionId, RSA rsa) | ||||
|     { | ||||
|         // Create the payload: just the session ID | ||||
|         var payloadBytes = sessionId.ToByteArray(); | ||||
|          | ||||
|         // Base64Url encode the payload | ||||
|         var payloadBase64 = Base64UrlEncode(payloadBytes); | ||||
|          | ||||
|         // Sign the payload with RSA-SHA256 | ||||
|         var signature = rsa.SignData(payloadBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); | ||||
|          | ||||
|         // Base64Url encode the signature | ||||
|         var signatureBase64 = Base64UrlEncode(signature); | ||||
|          | ||||
|         // Combine payload and signature with a dot | ||||
|         return $"{payloadBase64}.{signatureBase64}"; | ||||
|     } | ||||
|      | ||||
|     public bool ValidateToken(string token, out Guid sessionId) | ||||
|     { | ||||
|         sessionId = Guid.Empty; | ||||
|          | ||||
|         try | ||||
|         { | ||||
|             new("user_id", session.Account.Id.ToString()), | ||||
|             new("session_id", session.Id.ToString()) | ||||
|         }; | ||||
|  | ||||
|         var refreshTokenClaims = new JwtSecurityToken( | ||||
|             issuer: "solar-network", | ||||
|             audience: string.Join(',', session.Challenge.Audiences), | ||||
|             claims: claims, | ||||
|             expires: DateTime.Now.AddDays(30), | ||||
|             signingCredentials: creds | ||||
|         ); | ||||
|  | ||||
|         session.Challenge.Scopes.ForEach(c => claims.Add(new Claim("scope", c))); | ||||
|         if (session.Account.IsSuperuser) claims.Add(new Claim("is_superuser", "1")); | ||||
|         var accessTokenClaims = new JwtSecurityToken( | ||||
|             issuer: "solar-network", | ||||
|             audience: string.Join(',', session.Challenge.Audiences), | ||||
|             claims: claims, | ||||
|             expires: DateTime.Now.AddMinutes(30), | ||||
|             signingCredentials: creds | ||||
|         ); | ||||
|  | ||||
|         var handler = new JwtSecurityTokenHandler(); | ||||
|         var accessToken = handler.WriteToken(accessTokenClaims); | ||||
|         var refreshToken = handler.WriteToken(refreshTokenClaims); | ||||
|  | ||||
|         return new SignedTokenPair | ||||
|             // Split the token | ||||
|             var parts = token.Split('.'); | ||||
|             if (parts.Length != 2) | ||||
|                 return false; | ||||
|              | ||||
|             // Decode the payload | ||||
|             var payloadBytes = Base64UrlDecode(parts[0]); | ||||
|              | ||||
|             // Extract session ID | ||||
|             sessionId = new Guid(payloadBytes); | ||||
|              | ||||
|             // Load public key for verification | ||||
|             var publicKeyPem = File.ReadAllText(config["Jwt: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 | ||||
|         { | ||||
|             AccessToken = accessToken, | ||||
|             RefreshToken = refreshToken, | ||||
|             ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddMinutes(30)) | ||||
|         }; | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Helper methods for Base64Url encoding/decoding | ||||
|     private static string Base64UrlEncode(byte[] data) | ||||
|     { | ||||
|         return Convert.ToBase64String(data) | ||||
|             .TrimEnd('=') | ||||
|             .Replace('+', '-') | ||||
|             .Replace('/', '_'); | ||||
|     } | ||||
|      | ||||
|     private static byte[] Base64UrlDecode(string base64Url) | ||||
|     { | ||||
|         string padded = base64Url | ||||
|             .Replace('-', '+') | ||||
|             .Replace('_', '/'); | ||||
|              | ||||
|         switch (padded.Length % 4) | ||||
|         { | ||||
|             case 2: padded += "=="; break; | ||||
|             case 3: padded += "="; break; | ||||
|         } | ||||
|          | ||||
|         return Convert.FromBase64String(padded); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										94
									
								
								DysonNetwork.Sphere/Auth/CompactTokenService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								DysonNetwork.Sphere/Auth/CompactTokenService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| using System.Security.Cryptography; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Auth; | ||||
|  | ||||
| public class CompactTokenService(IConfiguration config) | ||||
| { | ||||
|     private readonly string _privateKeyPath = config["Jwt:PrivateKeyPath"]  | ||||
|         ?? throw new InvalidOperationException("Jwt:PrivateKeyPath configuration is missing"); | ||||
|      | ||||
|     public string CreateToken(Session session) | ||||
|     { | ||||
|         // Load the private key for signing | ||||
|         var privateKeyPem = File.ReadAllText(_privateKeyPath); | ||||
|         using var rsa = RSA.Create(); | ||||
|         rsa.ImportFromPem(privateKeyPem); | ||||
|          | ||||
|         // Create and return a single token | ||||
|         return CreateCompactToken(session.Id, rsa); | ||||
|     } | ||||
|  | ||||
|     private string CreateCompactToken(Guid sessionId, RSA rsa) | ||||
|     { | ||||
|         // Create the payload: just the session ID | ||||
|         var payloadBytes = sessionId.ToByteArray(); | ||||
|          | ||||
|         // Base64Url encode the payload | ||||
|         var payloadBase64 = Base64UrlEncode(payloadBytes); | ||||
|          | ||||
|         // Sign the payload with RSA-SHA256 | ||||
|         var signature = rsa.SignData(payloadBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); | ||||
|          | ||||
|         // Base64Url encode the signature | ||||
|         var signatureBase64 = Base64UrlEncode(signature); | ||||
|          | ||||
|         // Combine payload and signature with a dot | ||||
|         return $"{payloadBase64}.{signatureBase64}"; | ||||
|     } | ||||
|      | ||||
|     public bool ValidateToken(string token, out Guid sessionId) | ||||
|     { | ||||
|         sessionId = Guid.Empty; | ||||
|          | ||||
|         try | ||||
|         { | ||||
|             // Split the token | ||||
|             var parts = token.Split('.'); | ||||
|             if (parts.Length != 2) | ||||
|                 return false; | ||||
|              | ||||
|             // Decode the payload | ||||
|             var payloadBytes = Base64UrlDecode(parts[0]); | ||||
|              | ||||
|             // Extract session ID | ||||
|             sessionId = new Guid(payloadBytes); | ||||
|              | ||||
|             // Load public key for verification | ||||
|             var publicKeyPem = File.ReadAllText(config["Jwt: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; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Helper methods for Base64Url encoding/decoding | ||||
|     private static string Base64UrlEncode(byte[] data) | ||||
|     { | ||||
|         return Convert.ToBase64String(data) | ||||
|             .TrimEnd('=') | ||||
|             .Replace('+', '-') | ||||
|             .Replace('/', '_'); | ||||
|     } | ||||
|      | ||||
|     private static byte[] Base64UrlDecode(string base64Url) | ||||
|     { | ||||
|         string padded = base64Url | ||||
|             .Replace('-', '+') | ||||
|             .Replace('_', '/'); | ||||
|              | ||||
|         switch (padded.Length % 4) | ||||
|         { | ||||
|             case 2: padded += "=="; break; | ||||
|             case 3: padded += "="; break; | ||||
|         } | ||||
|          | ||||
|         return Convert.FromBase64String(padded); | ||||
|     } | ||||
| } | ||||
| @@ -1,40 +0,0 @@ | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Auth; | ||||
|  | ||||
| public class UserInfoMiddleware(RequestDelegate next, ICacheService cache) | ||||
| { | ||||
|     public async Task InvokeAsync(HttpContext context, AppDatabase db) | ||||
|     { | ||||
|         var sessionIdClaim = context.User.FindFirst("session_id")?.Value; | ||||
|         if (sessionIdClaim is not null && Guid.TryParse(sessionIdClaim, out var sessionId)) | ||||
|         { | ||||
|             var session = await cache.GetAsync<Session>($"Auth_{sessionId}"); | ||||
|             if (session is null) | ||||
|             { | ||||
|                 session = await db.AuthSessions | ||||
|                     .Where(e => e.Id == sessionId) | ||||
|                     .Include(e => e.Challenge) | ||||
|                     .Include(e => e.Account) | ||||
|                     .ThenInclude(e => e.Profile) | ||||
|                     .FirstOrDefaultAsync(); | ||||
|  | ||||
|                 if (session is not null) | ||||
|                 { | ||||
|                     await cache.SetWithGroupsAsync($"Auth_{sessionId}", session, | ||||
|                         [$"{AccountService.AccountCachePrefix}{session.Account.Id}"], TimeSpan.FromHours(1)); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (session is not null) | ||||
|             { | ||||
|                 context.Items["CurrentUser"] = session.Account; | ||||
|                 context.Items["CurrentSession"] = session; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         await next(context); | ||||
|     } | ||||
| } | ||||
| @@ -171,6 +171,7 @@ builder.Services.AddScoped<IWebSocketPacketHandler, MessageReadHandler>(); | ||||
| builder.Services.AddScoped<IWebSocketPacketHandler, MessageTypingHandler>(); | ||||
|  | ||||
| // Services | ||||
| builder.Services.AddScoped<CompactTokenService>(); | ||||
| builder.Services.AddScoped<RazorViewRenderer>(); | ||||
| builder.Services.Configure<GeoIpOptions>(builder.Configuration.GetSection("GeoIP")); | ||||
| builder.Services.AddScoped<GeoIpService>(); | ||||
| @@ -274,7 +275,6 @@ app.UseWebSockets(); | ||||
| app.UseRateLimiter(); | ||||
| app.UseHttpsRedirection(); | ||||
| app.UseAuthorization(); | ||||
| app.UseMiddleware<UserInfoMiddleware>(); | ||||
| app.UseMiddleware<PermissionMiddleware>(); | ||||
|  | ||||
| app.MapControllers().RequireRateLimiting("fixed"); | ||||
|   | ||||
| @@ -62,6 +62,7 @@ | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F42d8f09d6a294d00a6f49efc989927492fe00_003F4e_003F26d1ee34_003FQueryable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResizeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fef3339e864a448e2b1ec6fa7bbf4c6661fee00_003F48_003F0209e410_003FResizeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResourceManagerStringLocalizerFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb62f365d06c44ad695ff75960cdf97a2a800_003Fe4_003Ff6ba93b7_003FResourceManagerStringLocalizerFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARSA_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fee4f989f6b8042b59b2654fdc188e287243600_003F8b_003F44e5f855_003FRSA_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASafeHandle_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003F66_003Fde27c365_003FSafeHandle_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASecuritySchemeType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F29898ce74e3763a786ac1bd9a6db2152e1af75769440b1e53b9cbdf1dda1bd99_003FSecuritySchemeType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionContainerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc0e30e11d8f5456cb7a11b21ebee6c5a35c00_003F60_003F78b485f5_003FServiceCollectionContainerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user