diff --git a/DysonNetwork.Sphere/Auth/OidcProvider/Controllers/OidcProviderController.cs b/DysonNetwork.Sphere/Auth/OidcProvider/Controllers/OidcProviderController.cs index 910b7c5..2b9abdf 100644 --- a/DysonNetwork.Sphere/Auth/OidcProvider/Controllers/OidcProviderController.cs +++ b/DysonNetwork.Sphere/Auth/OidcProvider/Controllers/OidcProviderController.cs @@ -1,8 +1,5 @@ -using System.ComponentModel.DataAnnotations; -using System.Security.Claims; using System.Security.Cryptography; using System.Text; -using DysonNetwork.Sphere.Developer; using DysonNetwork.Sphere.Auth.OidcProvider.Options; using DysonNetwork.Sphere.Auth.OidcProvider.Responses; using DysonNetwork.Sphere.Auth.OidcProvider.Services; @@ -10,210 +7,104 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using System.Text.Json.Serialization; +using DysonNetwork.Sphere.Account; +using Microsoft.EntityFrameworkCore; namespace DysonNetwork.Sphere.Auth.OidcProvider.Controllers; -[Route("connect")] +[Route("/auth/open")] [ApiController] public class OidcProviderController( + AppDatabase db, OidcProviderService oidcService, + IConfiguration configuration, IOptions options, ILogger logger ) : ControllerBase { - [HttpGet("authorize")] - public async Task Authorize( - [Required][FromQuery(Name = "client_id")] Guid clientId, - [Required][FromQuery(Name = "response_type")] string responseType, - [FromQuery(Name = "redirect_uri")] string? redirectUri, - [FromQuery] string? scope, - [FromQuery] string? state, - [FromQuery] string? nonce, - [FromQuery(Name = "code_challenge")] string? codeChallenge, - [FromQuery(Name = "code_challenge_method")] string? codeChallengeMethod, - [FromQuery(Name = "response_mode")] string? responseMode - ) - { - // Check if user is authenticated - if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) - { - // Not authenticated - redirect to login with return URL - var loginUrl = "/Auth/Login"; - var returnUrl = $"{Request.Path}{Request.QueryString}"; - return Redirect($"{loginUrl}?returnUrl={Uri.EscapeDataString(returnUrl)}"); - } - - // Validate client - var client = await oidcService.FindClientByIdAsync(clientId); - if (client == null) - return BadRequest(new ErrorResponse { Error = "invalid_client", ErrorDescription = "Client not found" }); - - // Check if user has already granted permission to this client - // For now, we'll always show the consent page. In a real app, you might store consent decisions. - // If you want to implement "remember my decision", you would check that here. - var consentRequired = true; - - if (consentRequired) - { - // Redirect to consent page with all the OAuth parameters - var consentUrl = $"/Auth/Authorize?client_id={clientId}"; - if (!string.IsNullOrEmpty(responseType)) consentUrl += $"&response_type={Uri.EscapeDataString(responseType)}"; - if (!string.IsNullOrEmpty(redirectUri)) consentUrl += $"&redirect_uri={Uri.EscapeDataString(redirectUri)}"; - if (!string.IsNullOrEmpty(scope)) consentUrl += $"&scope={Uri.EscapeDataString(scope)}"; - if (!string.IsNullOrEmpty(state)) consentUrl += $"&state={Uri.EscapeDataString(state)}"; - if (!string.IsNullOrEmpty(nonce)) consentUrl += $"&nonce={Uri.EscapeDataString(nonce)}"; - if (!string.IsNullOrEmpty(codeChallenge)) consentUrl += $"&code_challenge={Uri.EscapeDataString(codeChallenge)}"; - if (!string.IsNullOrEmpty(codeChallengeMethod)) consentUrl += $"&code_challenge_method={Uri.EscapeDataString(codeChallengeMethod)}"; - if (!string.IsNullOrEmpty(responseMode)) consentUrl += $"&response_mode={Uri.EscapeDataString(responseMode)}"; - - return Redirect(consentUrl); - } - - // Skip redirect_uri validation for apps in Developing status - 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" }); - } - else - { - logger.LogWarning("Skipping redirect_uri validation for app {AppId} in Developing status", clientId); - - // If no redirect_uri is provided and we're in development, use the first one - if (string.IsNullOrEmpty(redirectUri) && client.RedirectUris?.Any() == true) - { - redirectUri = client.RedirectUris.First(); - } - } - - // Generate authorization code - var code = Guid.NewGuid().ToString("N"); - var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - - // In a real implementation, you'd store this code with the user's consent and requested scopes - // and validate it in the token endpoint - - // For now, we'll just return the code directly (simplified for example) - var response = new AuthorizationResponse - { - Code = code, - State = state, - Scope = scope, - Issuer = options.Value.IssuerUri - }; - - // Redirect back to the client with the authorization code - var finalRedirectUri = new UriBuilder(redirectUri ?? client.RedirectUris?.First() ?? throw new InvalidOperationException("No redirect URI provided and no default redirect URI found")); - var query = System.Web.HttpUtility.ParseQueryString(finalRedirectUri.Query); - query["code"] = response.Code; - if (!string.IsNullOrEmpty(response.State)) - query["state"] = response.State; - if (!string.IsNullOrEmpty(response.Scope)) - query["scope"] = response.Scope; - - finalRedirectUri.Query = query.ToString(); - return Redirect(finalRedirectUri.Uri.ToString()); - } - [HttpPost("token")] [Consumes("application/x-www-form-urlencoded")] public async Task Token([FromForm] TokenRequest request) { - if (request.GrantType == "authorization_code") + switch (request.GrantType) { // Validate client credentials - if (request.ClientId == null || string.IsNullOrEmpty(request.ClientSecret)) - return BadRequest(new ErrorResponse { Error = "invalid_client", ErrorDescription = "Client credentials are required" }); - - var client = await oidcService.FindClientByIdAsync(request.ClientId.Value); - if (client == null || !await oidcService.ValidateClientCredentialsAsync(request.ClientId.Value, request.ClientSecret)) - return BadRequest(new ErrorResponse { Error = "invalid_client", ErrorDescription = "Invalid client credentials" }); - - // Validate the authorization code - var authCode = await oidcService.ValidateAuthorizationCodeAsync( - request.Code ?? string.Empty, - request.ClientId.Value, - request.RedirectUri, - request.CodeVerifier); - - if (authCode == null) + case "authorization_code" when request.ClientId == null || string.IsNullOrEmpty(request.ClientSecret): + return BadRequest("Client credentials are required"); + case "authorization_code" when request.Code == null: + return BadRequest("Authorization code is required"); + case "authorization_code": { - logger.LogWarning("Invalid or expired authorization code: {Code}", request.Code); - return BadRequest(new ErrorResponse { Error = "invalid_grant", ErrorDescription = "Invalid or expired authorization code" }); + var client = await oidcService.FindClientByIdAsync(request.ClientId.Value); + if (client == null || + !await oidcService.ValidateClientCredentialsAsync(request.ClientId.Value, request.ClientSecret)) + return BadRequest(new ErrorResponse + { Error = "invalid_client", ErrorDescription = "Invalid client credentials" }); + + // Validate the authorization code + var authCode = await oidcService.ValidateAuthorizationCodeAsync( + request.Code ?? string.Empty, + request.ClientId.Value, + request.RedirectUri, + request.CodeVerifier + ); + + if (authCode == null) + { + logger.LogWarning(@"Invalid or expired authorization code: {Code}", request.Code); + return BadRequest(new ErrorResponse + { Error = "invalid_grant", ErrorDescription = "Invalid or expired authorization code" }); + } + + // Generate tokens + var tokenResponse = await oidcService.GenerateTokenResponseAsync( + clientId: request.ClientId.Value, + scopes: authCode.Scopes, + authorizationCode: request.Code! + ); + + return Ok(tokenResponse); } - - // Generate tokens - var tokenResponse = await oidcService.GenerateTokenResponseAsync( - clientId: request.ClientId.Value, - subjectId: authCode.UserId, - scopes: authCode.Scopes, - authorizationCode: request.Code); - - return Ok(tokenResponse); + case "refresh_token": + // Handle refresh token request + // In a real implementation, you would validate the refresh token + // and issue a new access token + return BadRequest(new ErrorResponse { Error = "unsupported_grant_type" }); + default: + return BadRequest(new ErrorResponse { Error = "unsupported_grant_type" }); } - else if (request.GrantType == "refresh_token") - { - // Handle refresh token request - // In a real implementation, you would validate the refresh token - // and issue a new access token - return BadRequest(new ErrorResponse { Error = "unsupported_grant_type" }); - } - - return BadRequest(new ErrorResponse { Error = "unsupported_grant_type" }); } [HttpGet("userinfo")] - [Authorize(AuthenticationSchemes = "Bearer")] + [Authorize] public async Task UserInfo() { - var authHeader = HttpContext.Request.Headers.Authorization.ToString(); - if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ")) - { - var loginUrl = "/Account/Login"; // Update this path to your actual login page path - var returnUrl = $"{Request.Scheme}://{Request.Host}{Request.Path}{Request.QueryString}"; - return Redirect($"{loginUrl}?returnUrl={Uri.EscapeDataString(returnUrl)}"); - } - - var token = authHeader["Bearer ".Length..].Trim(); - var jwtToken = oidcService.ValidateToken(token); - - if (jwtToken == null) - { - var loginUrl = "/Account/Login"; // Update this path to your actual login page path - var returnUrl = $"{Request.Scheme}://{Request.Host}{Request.Path}{Request.QueryString}"; - return Redirect($"{loginUrl}?returnUrl={Uri.EscapeDataString(returnUrl)}"); - } - - // Get user info based on the subject claim from the token - var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - var userName = User.FindFirstValue(ClaimTypes.Name); - var userEmail = User.FindFirstValue(ClaimTypes.Email); + if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser || + HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); // Get requested scopes from the token - var scopes = jwtToken.Claims - .Where(c => c.Type == "scope") - .SelectMany(c => c.Value.Split(' ')) - .ToHashSet(); + var scopes = currentSession.Challenge.Scopes; var userInfo = new Dictionary { - ["sub"] = userId ?? "anonymous" + ["sub"] = currentUser.Id }; // Include standard claims based on scopes if (scopes.Contains("profile") || scopes.Contains("name")) { - if (!string.IsNullOrEmpty(userName)) - userInfo["name"] = userName; + userInfo["name"] = currentUser.Name; + userInfo["preferred_username"] = currentUser.Nick; } - if (scopes.Contains("email") && !string.IsNullOrEmpty(userEmail)) + var userEmail = await db.AccountContacts + .Where(c => c.Type == AccountContactType.Email && c.AccountId == currentUser.Id) + .FirstOrDefaultAsync(); + if (scopes.Contains("email") && userEmail is not null) { - userInfo["email"] = userEmail; - userInfo["email_verified"] = true; // In a real app, check if email is verified + userInfo["email"] = userEmail.Content; + userInfo["email_verified"] = userEmail.VerifiedAt is not null; } return Ok(userInfo); @@ -222,18 +113,19 @@ public class OidcProviderController( [HttpGet(".well-known/openid-configuration")] public IActionResult GetConfiguration() { - var baseUrl = $"{Request.Scheme}://{Request.Host}{Request.PathBase}".TrimEnd('/'); + var baseUrl = configuration["BaseUrl"]; var issuer = options.Value.IssuerUri.TrimEnd('/'); return Ok(new { issuer = issuer, authorization_endpoint = $"{baseUrl}/connect/authorize", - token_endpoint = $"{baseUrl}/connect/token", - userinfo_endpoint = $"{baseUrl}/connect/userinfo", + token_endpoint = $"{baseUrl}/auth/open/token", + userinfo_endpoint = $"{baseUrl}/auth/open/userinfo", jwks_uri = $"{baseUrl}/.well-known/openid-configuration/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" }, + response_types_supported = new[] + { "code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token" }, grant_types_supported = new[] { "authorization_code", "refresh_token" }, token_endpoint_auth_methods_supported = new[] { "client_secret_basic", "client_secret_post" }, id_token_signing_alg_values_supported = new[] { "HS256" }, @@ -248,12 +140,8 @@ public class OidcProviderController( } [HttpGet("jwks")] - public IActionResult Jwks() + public IActionResult GetJwks() { - // In a production environment, you should use asymmetric keys (RSA or EC) - // and expose only the public key here. This is a simplified example using HMAC. - // For production, consider using RSA or EC keys and proper key rotation. - var keyBytes = Encoding.UTF8.GetBytes(options.Value.SigningKey); var keyId = Convert.ToBase64String(SHA256.HashData(keyBytes)[..8]) .Replace("+", "-") @@ -279,27 +167,19 @@ public class OidcProviderController( public class TokenRequest { - [JsonPropertyName("grant_type")] - public string? GrantType { get; set; } - - [JsonPropertyName("code")] - public string? Code { get; set; } - - [JsonPropertyName("redirect_uri")] - public string? RedirectUri { get; set; } - - [JsonPropertyName("client_id")] - public Guid? ClientId { get; set; } - - [JsonPropertyName("client_secret")] - public string? ClientSecret { get; set; } - - [JsonPropertyName("refresh_token")] - public string? RefreshToken { get; set; } - - [JsonPropertyName("scope")] - public string? Scope { get; set; } - - [JsonPropertyName("code_verifier")] - public string? CodeVerifier { get; set; } + [JsonPropertyName("grant_type")] public string? GrantType { get; set; } + + [JsonPropertyName("code")] public string? Code { get; set; } + + [JsonPropertyName("redirect_uri")] public string? RedirectUri { get; set; } + + [JsonPropertyName("client_id")] public Guid? ClientId { get; set; } + + [JsonPropertyName("client_secret")] public string? ClientSecret { get; set; } + + [JsonPropertyName("refresh_token")] public string? RefreshToken { get; set; } + + [JsonPropertyName("scope")] public string? Scope { get; set; } + + [JsonPropertyName("code_verifier")] public string? CodeVerifier { get; set; } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Auth/OidcProvider/Models/AuthorizationCodeInfo.cs b/DysonNetwork.Sphere/Auth/OidcProvider/Models/AuthorizationCodeInfo.cs index 7e3795a..fafc970 100644 --- a/DysonNetwork.Sphere/Auth/OidcProvider/Models/AuthorizationCodeInfo.cs +++ b/DysonNetwork.Sphere/Auth/OidcProvider/Models/AuthorizationCodeInfo.cs @@ -7,7 +7,7 @@ namespace DysonNetwork.Sphere.Auth.OidcProvider.Models; public class AuthorizationCodeInfo { public Guid ClientId { get; set; } - public string UserId { get; set; } = string.Empty; + public Guid AccountId { get; set; } public string RedirectUri { get; set; } = string.Empty; public List Scopes { get; set; } = new(); public string? CodeChallenge { get; set; } diff --git a/DysonNetwork.Sphere/Auth/OidcProvider/Services/OidcProviderService.cs b/DysonNetwork.Sphere/Auth/OidcProvider/Services/OidcProviderService.cs index 7a9188c..99293f9 100644 --- a/DysonNetwork.Sphere/Auth/OidcProvider/Services/OidcProviderService.cs +++ b/DysonNetwork.Sphere/Auth/OidcProvider/Services/OidcProviderService.cs @@ -2,13 +2,11 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; -using System.Text.Encodings.Web; -using System.Text.Json; using DysonNetwork.Sphere.Auth.OidcProvider.Models; using DysonNetwork.Sphere.Auth.OidcProvider.Options; using DysonNetwork.Sphere.Auth.OidcProvider.Responses; using DysonNetwork.Sphere.Developer; -using Microsoft.AspNetCore.Identity; +using DysonNetwork.Sphere.Storage; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; @@ -18,7 +16,8 @@ namespace DysonNetwork.Sphere.Auth.OidcProvider.Services; public class OidcProviderService( AppDatabase db, - IClock clock, + AuthService auth, + ICacheService cache, IOptions options, ILogger logger ) @@ -44,6 +43,7 @@ public class OidcProviderService( var client = await FindClientByIdAsync(clientId); if (client == null) return false; + var clock = SystemClock.Instance; var secret = client.Secrets .Where(s => s.IsOidc && (s.ExpiredAt == null || s.ExpiredAt > clock.GetCurrentInstant())) .FirstOrDefault(s => s.Secret == clientSecret); // In production, use proper hashing @@ -53,20 +53,28 @@ public class OidcProviderService( public async Task GenerateTokenResponseAsync( Guid clientId, - string subjectId, - IEnumerable? scopes = null, - string? authorizationCode = null) + string authorizationCode, + IEnumerable? scopes = null + ) { var client = await FindClientByIdAsync(clientId); if (client == null) throw new InvalidOperationException("Client not found"); + var authCode = await ValidateAuthorizationCodeAsync(authorizationCode, clientId); + if (authCode is null) throw new InvalidOperationException("Invalid authorization code"); + var account = await db.Accounts.Where(a => a.Id == authCode.AccountId).FirstOrDefaultAsync(); + if (account is null) throw new InvalidOperationException("Account was not found"); + + var clock = SystemClock.Instance; var now = clock.GetCurrentInstant(); + var session = await auth.CreateSessionAsync(account, now); + var expiresIn = (int)_options.AccessTokenLifetime.TotalSeconds; var expiresAt = now.Plus(Duration.FromSeconds(expiresIn)); // Generate access token - var accessToken = GenerateJwtToken(client, subjectId, expiresAt, scopes); + var accessToken = GenerateJwtToken(client, session, expiresAt, scopes); var refreshToken = GenerateRefreshToken(); // In a real implementation, you would store the token in the database @@ -83,27 +91,33 @@ public class OidcProviderService( }; } - private string GenerateJwtToken(CustomApp client, string subjectId, Instant expiresAt, IEnumerable? scopes = null) + private string GenerateJwtToken( + CustomApp client, + Session session, + Instant expiresAt, + IEnumerable? scopes = null + ) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_options.SigningKey); + var clock = SystemClock.Instance; var tokenDescriptor = new SecurityTokenDescriptor { - Subject = new ClaimsIdentity(new[] - { - new Claim(JwtRegisteredClaimNames.Sub, subjectId), + 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()) - }), + ]), Expires = expiresAt.ToDateTimeUtc(), Issuer = _options.IssuerUri, Audience = client.Id.ToString(), SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(key), - SecurityAlgorithms.HmacSha256Signature) + SecurityAlgorithms.HmacSha256Signature + ) }; // Add scopes as claims if provided, otherwise use client's default scopes @@ -163,11 +177,11 @@ public class OidcProviderService( } } - private static readonly Dictionary _authorizationCodes = new(); - + // Authorization codes are now managed through ICacheService + public async Task GenerateAuthorizationCodeAsync( Guid clientId, - string userId, + Guid userId, string redirectUri, IEnumerable scopes, string? codeChallenge = null, @@ -175,14 +189,15 @@ public class OidcProviderService( string? nonce = null) { // Generate a random code + var clock = SystemClock.Instance; var code = GenerateRandomString(32); var now = clock.GetCurrentInstant(); - - // Store the code with its metadata - _authorizationCodes[code] = new AuthorizationCodeInfo + + // Create the authorization code info + var authCodeInfo = new AuthorizationCodeInfo { ClientId = clientId, - UserId = userId, + AccountId = userId, RedirectUri = redirectUri, Scopes = scopes.ToList(), CodeChallenge = codeChallenge, @@ -191,48 +206,58 @@ public class OidcProviderService( Expiration = now.Plus(Duration.FromTimeSpan(_options.AuthorizationCodeLifetime)), 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, userId); return code; } - + public async Task ValidateAuthorizationCodeAsync( string code, Guid clientId, string? redirectUri = null, - string? codeVerifier = null) + string? codeVerifier = null + ) { - if (!_authorizationCodes.TryGetValue(code, out var authCode) || authCode == null) + var cacheKey = $"auth:code:{code}"; + var (found, authCode) = await cache.GetAsyncWithStatus(cacheKey); + + if (!found || authCode == null) { logger.LogWarning("Authorization code not found: {Code}", code); return null; } - + + var clock = SystemClock.Instance; var now = clock.GetCurrentInstant(); - - // Check if code has expired + + // Check if the code has expired if (now > authCode.Expiration) { logger.LogWarning("Authorization code expired: {Code}", code); - _authorizationCodes.Remove(code); + await cache.RemoveAsync(cacheKey); return null; } - + // Verify client ID matches if (authCode.ClientId != clientId) { - logger.LogWarning("Client ID mismatch for code {Code}. Expected: {ExpectedClientId}, Actual: {ActualClientId}", + logger.LogWarning( + "Client ID mismatch for code {Code}. Expected: {ExpectedClientId}, Actual: {ActualClientId}", code, authCode.ClientId, clientId); return null; } - + // Verify redirect URI if provided if (!string.IsNullOrEmpty(redirectUri) && authCode.RedirectUri != redirectUri) { logger.LogWarning("Redirect URI mismatch for code {Code}", code); return null; } - + // Verify PKCE code challenge if one was provided during authorization if (!string.IsNullOrEmpty(authCode.CodeChallenge)) { @@ -241,33 +266,33 @@ public class OidcProviderService( logger.LogWarning("PKCE code verifier is required but not provided for code {Code}", code); return null; } - + var isValid = authCode.CodeChallengeMethod?.ToUpperInvariant() switch { "S256" => VerifyCodeChallenge(codeVerifier, authCode.CodeChallenge, "S256"), "PLAIN" => VerifyCodeChallenge(codeVerifier, authCode.CodeChallenge, "PLAIN"), _ => false // Unsupported code challenge method }; - + if (!isValid) { logger.LogWarning("PKCE code verifier validation failed for code {Code}", code); return null; } } - - // Code is valid, remove it from the store (codes are single-use) - _authorizationCodes.Remove(code); - + + // Code is valid, remove it from the cache (codes are single-use) + await cache.RemoveAsync(cacheKey); + return authCode; } - + private static string GenerateRandomString(int length) { const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"; var random = RandomNumberGenerator.Create(); var result = new char[length]; - + for (int i = 0; i < length; i++) { var randomNumber = new byte[4]; @@ -275,14 +300,14 @@ public class OidcProviderService( var index = (int)(BitConverter.ToUInt32(randomNumber, 0) % chars.Length); result[i] = chars[index]; } - + return new string(result); } - + private static bool VerifyCodeChallenge(string codeVerifier, string codeChallenge, string method) { if (string.IsNullOrEmpty(codeVerifier)) return false; - + if (method == "S256") { using var sha256 = SHA256.Create(); @@ -290,12 +315,12 @@ public class OidcProviderService( var base64 = Base64UrlEncoder.Encode(hash); return string.Equals(base64, codeChallenge, StringComparison.Ordinal); } - + if (method == "PLAIN") { return string.Equals(codeVerifier, codeChallenge, StringComparison.Ordinal); } - + return false; } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Pages/Auth/Authorize.cshtml.cs b/DysonNetwork.Sphere/Pages/Auth/Authorize.cshtml.cs index b626486..4b7b3ea 100644 --- a/DysonNetwork.Sphere/Pages/Auth/Authorize.cshtml.cs +++ b/DysonNetwork.Sphere/Pages/Auth/Authorize.cshtml.cs @@ -4,6 +4,8 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using DysonNetwork.Sphere.Auth.OidcProvider.Services; using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; +using DysonNetwork.Sphere.Auth.OidcProvider.Responses; +using DysonNetwork.Sphere.Developer; namespace DysonNetwork.Sphere.Pages.Auth; @@ -48,9 +50,15 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel public string? AppLogo { get; set; } public string? AppUri { get; set; } public string[]? RequestedScopes { get; set; } - + public async Task OnGetAsync() { + if (HttpContext.Items["CurrentUser"] is not Sphere.Account.Account) + { + var returnUrl = Uri.EscapeDataString($"{Request.Path}{Request.QueryString}"); + return RedirectToPage($"/Auth/Login?returnUrl={returnUrl}"); + } + if (string.IsNullOrEmpty(ClientIdString) || !Guid.TryParse(ClientIdString, out var clientId)) { ModelState.AddModelError("client_id", "Invalid client_id format"); @@ -65,6 +73,14 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel ModelState.AddModelError("client_id", "Client not found"); return NotFound("Client not found"); } + + 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" }); + } AppName = client.Name; AppLogo = client.LogoUri; @@ -76,7 +92,9 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel public async Task OnPostAsync(bool allow) { - // First validate the client ID + if (HttpContext.Items["CurrentUser"] is not Sphere.Account.Account currentUser) return Unauthorized(); + + // First, validate the client ID if (string.IsNullOrEmpty(ClientIdString) || !Guid.TryParse(ClientIdString, out var clientId)) { ModelState.AddModelError("client_id", "Invalid client_id format"); @@ -85,7 +103,7 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel ClientId = clientId; - // Check if client exists + // Check if a client exists var client = await oidcService.FindClientByIdAsync(ClientId); if (client == null) { @@ -119,7 +137,7 @@ public class AuthorizeModel(OidcProviderService oidcService) : PageModel // Generate authorization code var authCode = await oidcService.GenerateAuthorizationCodeAsync( clientId: ClientId, - userId: User.Identity?.Name ?? string.Empty, + userId: currentUser.Id, redirectUri: RedirectUri, scopes: Scope?.Split(' ', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(), codeChallenge: CodeChallenge,