using System.Text; using MediatR; using Web.Api.Common; using Web.Api.Extensions; namespace Web.Api.Endpoints.OAuth; /// /// POST /oauth/token — RFC 6749 §4.4 Client Credentials grant. /// Body: application/x-www-form-urlencoded (grant_type, client_id, client_secret, scope) /// 또는 HTTP Basic auth (Authorization: Basic base64(client_id:client_secret)) + form 의 grant_type/scope. /// internal sealed class Token : IEndpoint { public void MapEndpoint(IEndpointRouteBuilder app) { app.MapPost("oauth/token", async ( HttpContext httpContext, ISender sender, CancellationToken ct ) => { if (!httpContext.Request.HasFormContentType) { return CustomResults.Problem(SharedKernel.Results.Result.Failure(SharedKernel.Results.Error.Problem("OAuth.InvalidRequest", "Content-Type must be application/x-www-form-urlencoded."))); } var form = await httpContext.Request.ReadFormAsync(ct); string? clientID = form["client_id"]; string? clientSecret = form["client_secret"]; // HTTP Basic auth fallback (RFC 6749 §2.3.1) if (string.IsNullOrEmpty(clientID)) { var auth = httpContext.Request.Headers.Authorization.ToString(); if (auth.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) { try { var base64 = auth["Basic ".Length..]; var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(base64)); var sep = decoded.IndexOf(':'); if (sep > 0) { clientID = decoded[..sep]; clientSecret = decoded[(sep + 1)..]; } } catch (FormatException) { // ignore — invalid base64 falls through to validation } } } var command = new Application.Features.Api.OAuth.IssueToken.Command( form["grant_type"].ToString(), clientID, clientSecret, form["scope"].ToString() ); var result = await sender.Send(command, ct); return result.Match( data => Results.Ok(data), CustomResults.Problem ); }) .WithTags("OAuth") .WithGroupName("public") .AllowAnonymous(); } }