2025-06-29 11:53:44 +08:00

185 lines
7.1 KiB
C#

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;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Controllers;
[Route("/auth/open")]
[ApiController]
public class OidcProviderController(
AppDatabase db,
OidcProviderService oidcService,
IConfiguration configuration,
IOptions<OidcProviderOptions> options,
ILogger<OidcProviderController> logger
)
: ControllerBase
{
[HttpPost("token")]
[Consumes("application/x-www-form-urlencoded")]
public async Task<IActionResult> 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<IActionResult> 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<string, object>
{
["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}/connect/authorize",
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" },
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("jwks")]
public IActionResult GetJwks()
{
var keyBytes = Encoding.UTF8.GetBytes(options.Value.SigningKey);
var keyId = Convert.ToBase64String(SHA256.HashData(keyBytes)[..8])
.Replace("+", "-")
.Replace("/", "_")
.Replace("=", "");
return Ok(new
{
keys = new[]
{
new
{
kty = "oct",
use = "sig",
kid = keyId,
k = Convert.ToBase64String(keyBytes),
alg = "HS256"
}
}
});
}
}
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; }
}