From c0879d30d457af14435fe3df100598f1df4a7dc5 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 29 Jun 2025 17:46:17 +0800 Subject: [PATCH] :sparkles: Oidc auto approval and session reuse --- .../Services/OidcProviderService.cs | 79 ++++++++++++++++--- .../Pages/Auth/Authorize.cshtml.cs | 78 ++++++++++++++++-- 2 files changed, 138 insertions(+), 19 deletions(-) diff --git a/DysonNetwork.Sphere/Auth/OidcProvider/Services/OidcProviderService.cs b/DysonNetwork.Sphere/Auth/OidcProvider/Services/OidcProviderService.cs index ff6784e..2fd8614 100644 --- a/DysonNetwork.Sphere/Auth/OidcProvider/Services/OidcProviderService.cs +++ b/DysonNetwork.Sphere/Auth/OidcProvider/Services/OidcProviderService.cs @@ -38,6 +38,20 @@ public class OidcProviderService( .FirstOrDefaultAsync(c => c.Id == appId); } + public async Task FindValidSessionAsync(Guid accountId, Guid clientId) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + return await db.AuthSessions + .Include(s => s.Challenge) + .Where(s => s.AccountId == accountId && + s.AppId == clientId && + (s.ExpiredAt == null || s.ExpiredAt > now) && + s.Challenge.Type == ChallengeType.OAuth) + .OrderByDescending(s => s.CreatedAt) + .FirstOrDefaultAsync(); + } + public async Task ValidateClientCredentialsAsync(Guid clientId, string clientSecret) { var client = await FindClientByIdAsync(clientId); @@ -76,15 +90,15 @@ public class OidcProviderService( var account = await db.Accounts.Where(a => a.Id == authCode.AccountId).FirstOrDefaultAsync(); if (account is null) throw new InvalidOperationException("Account was not found"); - session = await auth.CreateSessionForOidcAsync(account, now); + session = await auth.CreateSessionForOidcAsync(account, now, client.Id); scopes = authCode.Scopes; } else if (sessionId.HasValue) { // Refresh token flow - session = await FindSessionByIdAsync(sessionId.Value) ?? - throw new InvalidOperationException("Invalid session"); - + session = await FindSessionByIdAsync(sessionId.Value) ?? + throw new InvalidOperationException("Invalid session"); + // Verify the session is still valid if (session.ExpiredAt < now) throw new InvalidOperationException("Session has expired"); @@ -112,10 +126,10 @@ public class OidcProviderService( } private string GenerateJwtToken( - CustomApp client, - Session session, - Instant expiresAt, - IEnumerable? scopes = null + CustomApp client, + Session session, + Instant expiresAt, + IEnumerable? scopes = null ) { var tokenHandler = new JwtSecurityTokenHandler(); @@ -126,10 +140,10 @@ public class OidcProviderService( { Subject = new ClaimsIdentity([ new Claim(JwtRegisteredClaimNames.Sub, session.AccountId.ToString()), - new Claim(JwtRegisteredClaimNames.Jti, session.Id.ToString()), - new Claim(JwtRegisteredClaimNames.Iat, now.ToUnixTimeSeconds().ToString(), - ClaimValueTypes.Integer64), - new Claim("client_id", client.Id.ToString()) + new Claim(JwtRegisteredClaimNames.Jti, session.Id.ToString()), + new Claim(JwtRegisteredClaimNames.Iat, now.ToUnixTimeSeconds().ToString(), + ClaimValueTypes.Integer64), + new Claim("client_id", client.Id.ToString()) ]), Expires = expiresAt.ToDateTimeUtc(), Issuer = _options.IssuerUri, @@ -207,6 +221,44 @@ public class OidcProviderService( return string.Equals(secret, hashedSecret, StringComparison.Ordinal); } + public async Task GenerateAuthorizationCodeForExistingSessionAsync( + Session session, + Guid clientId, + string redirectUri, + IEnumerable scopes, + string? codeChallenge = null, + string? codeChallengeMethod = null, + string? nonce = null) + { + var clock = SystemClock.Instance; + var now = clock.GetCurrentInstant(); + var code = Guid.NewGuid().ToString("N"); + + // Update the session's last activity time + await db.AuthSessions.Where(s => s.Id == session.Id) + .ExecuteUpdateAsync(s => s.SetProperty(s => s.LastGrantedAt, now)); + + // Create the authorization code info + var authCodeInfo = new AuthorizationCodeInfo + { + ClientId = clientId, + AccountId = session.AccountId, + RedirectUri = redirectUri, + Scopes = scopes.ToList(), + CodeChallenge = codeChallenge, + CodeChallengeMethod = codeChallengeMethod, + Nonce = nonce, + CreatedAt = now + }; + + // Store the code with its metadata in the cache + var cacheKey = $"auth:code:{code}"; + await cache.SetAsync(cacheKey, authCodeInfo, _options.AuthorizationCodeLifetime); + + logger.LogInformation("Generated authorization code for client {ClientId} and user {UserId}", clientId, session.AccountId); + return code; + } + public async Task GenerateAuthorizationCodeAsync( Guid clientId, Guid userId, @@ -214,7 +266,8 @@ public class OidcProviderService( IEnumerable scopes, string? codeChallenge = null, string? codeChallengeMethod = null, - string? nonce = null) + string? nonce = null + ) { // Generate a random code var clock = SystemClock.Instance; diff --git a/DysonNetwork.Sphere/Pages/Auth/Authorize.cshtml.cs b/DysonNetwork.Sphere/Pages/Auth/Authorize.cshtml.cs index 55e9cca..e1f68a7 100644 --- a/DysonNetwork.Sphere/Pages/Auth/Authorize.cshtml.cs +++ b/DysonNetwork.Sphere/Pages/Auth/Authorize.cshtml.cs @@ -1,9 +1,8 @@ -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using DysonNetwork.Sphere.Auth.OidcProvider.Services; -using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; +using DysonNetwork.Sphere.Auth; using DysonNetwork.Sphere.Auth.OidcProvider.Responses; using DysonNetwork.Sphere.Developer; @@ -48,12 +47,14 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel public async Task OnGetAsync() { - if (HttpContext.Items["CurrentUser"] is not Sphere.Account.Account) + // First check if user is authenticated + if (HttpContext.Items["CurrentUser"] is not Sphere.Account.Account currentUser) { var returnUrl = Uri.EscapeDataString($"{Request.Path}{Request.QueryString}"); return RedirectToPage("/Auth/Login", new { returnUrl }); } + // Validate client_id if (string.IsNullOrEmpty(ClientIdString) || !Guid.TryParse(ClientIdString, out var clientId)) { ModelState.AddModelError("client_id", "Invalid client_id format"); @@ -62,6 +63,7 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel ClientId = clientId; + // Get client info var client = await oidcService.FindClientByIdAsync(ClientId); if (client == null) { @@ -69,14 +71,28 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel return NotFound("Client not found"); } + // Validate redirect URI for non-Developing apps if (client.Status != CustomAppStatus.Developing) { - // Validate redirect URI for non-Developing apps if (!string.IsNullOrEmpty(RedirectUri) && !(client.RedirectUris?.Contains(RedirectUri) ?? false)) - return BadRequest( - new ErrorResponse { Error = "invalid_request", ErrorDescription = "Invalid redirect_uri" }); + { + return BadRequest(new ErrorResponse + { + Error = "invalid_request", + ErrorDescription = "Invalid redirect_uri" + }); + } } + // Check for an existing valid session + var existingSession = await oidcService.FindValidSessionAsync(currentUser.Id, clientId); + if (existingSession != null) + { + // Auto-approve since valid session exists + return await HandleApproval(currentUser, client, existingSession); + } + + // Show authorization page AppName = client.Name; AppLogo = client.LogoUri; AppUri = client.ClientUri; @@ -85,6 +101,56 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel return Page(); } + private async Task HandleApproval(Sphere.Account.Account currentUser, CustomApp client, Session? existingSession = null) + { + if (string.IsNullOrEmpty(RedirectUri)) + { + ModelState.AddModelError("redirect_uri", "No redirect_uri provided"); + return BadRequest("No redirect_uri provided"); + } + + string authCode; + + if (existingSession != null) + { + // Reuse existing session + authCode = await oidcService.GenerateAuthorizationCodeForExistingSessionAsync( + session: existingSession, + clientId: ClientId, + redirectUri: RedirectUri, + scopes: Scope?.Split(' ', StringSplitOptions.RemoveEmptyEntries) ?? [], + codeChallenge: CodeChallenge, + codeChallengeMethod: CodeChallengeMethod, + nonce: Nonce + ); + } + else + { + // Create new session (existing flow) + authCode = await oidcService.GenerateAuthorizationCodeAsync( + clientId: ClientId, + userId: currentUser.Id, + redirectUri: RedirectUri, + scopes: Scope?.Split(' ', StringSplitOptions.RemoveEmptyEntries) ?? [], + codeChallenge: CodeChallenge, + codeChallengeMethod: CodeChallengeMethod, + nonce: Nonce + ); + } + + // Build the redirect URI with the authorization code + var redirectUriBuilder = new UriBuilder(RedirectUri); + var query = System.Web.HttpUtility.ParseQueryString(redirectUriBuilder.Query); + query["code"] = authCode; + if (!string.IsNullOrEmpty(State)) + query["state"] = State; + if (!string.IsNullOrEmpty(Scope)) + query["scope"] = Scope; + redirectUriBuilder.Query = query.ToString(); + + return Redirect(redirectUriBuilder.ToString()); + } + public async Task OnPostAsync(bool allow) { if (HttpContext.Items["CurrentUser"] is not Sphere.Account.Account currentUser) return Unauthorized();