using System.Security.Cryptography; using System.Text; using DysonNetwork.Sphere.Auth.OidcProvider.Options; using DysonNetwork.Sphere.Auth.OidcProvider.Responses; using DysonNetwork.Sphere.Auth.OidcProvider.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; 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; [Route("/auth/open")] [ApiController] public class OidcProviderController( AppDatabase db, OidcProviderService oidcService, IConfiguration configuration, IOptions options, ILogger logger ) : ControllerBase { [HttpPost("token")] [Consumes("application/x-www-form-urlencoded")] public async Task Token([FromForm] TokenRequest request) { switch (request.GrantType) { // Validate client credentials 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": { 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); } 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" }); } } [HttpGet("userinfo")] [Authorize] public async Task UserInfo() { 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 = currentSession.Challenge.Scopes; var userInfo = new Dictionary { ["sub"] = currentUser.Id }; // Include standard claims based on scopes if (scopes.Contains("profile") || scopes.Contains("name")) { userInfo["name"] = currentUser.Name; userInfo["preferred_username"] = currentUser.Nick; } 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.Content; userInfo["email_verified"] = userEmail.VerifiedAt is not null; } return Ok(userInfo); } [HttpGet("/.well-known/openid-configuration")] public IActionResult GetConfiguration() { var baseUrl = configuration["BaseUrl"]; var issuer = options.Value.IssuerUri.TrimEnd('/'); return Ok(new { issuer = issuer, authorization_endpoint = $"{baseUrl}/auth/authorize", token_endpoint = $"{baseUrl}/auth/open/token", userinfo_endpoint = $"{baseUrl}/auth/open/userinfo", 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" }, 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" }, subject_types_supported = new[] { "public" }, claims_supported = new[] { "sub", "name", "email", "email_verified" }, code_challenge_methods_supported = new[] { "S256" }, response_modes_supported = new[] { "query", "fragment", "form_post" }, request_parameter_supported = true, request_uri_parameter_supported = true, require_request_uri_registration = false }); } [HttpGet("/.well-known/jwks")] public IActionResult GetJwks() { 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("=", ""); return Ok(new { keys = new[] { new { kty = "RSA", use = "sig", kid = keyId, n = Base64UrlEncoder.Encode(parameters.Modulus!), e = Base64UrlEncoder.Encode(parameters.Exponent!), alg = "RS256" } } }); } } 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; } }