Token.cs 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. using System.Text;
  2. using MediatR;
  3. using Web.Api.Common;
  4. using Web.Api.Extensions;
  5. namespace Web.Api.Endpoints.OAuth;
  6. /// <summary>
  7. /// POST /oauth/token — RFC 6749 §4.4 Client Credentials grant.
  8. /// Body: application/x-www-form-urlencoded (grant_type, client_id, client_secret, scope)
  9. /// 또는 HTTP Basic auth (Authorization: Basic base64(client_id:client_secret)) + form 의 grant_type/scope.
  10. /// </summary>
  11. internal sealed class Token : IEndpoint
  12. {
  13. public void MapEndpoint(IEndpointRouteBuilder app)
  14. {
  15. app.MapPost("oauth/token", async (
  16. HttpContext httpContext,
  17. ISender sender,
  18. CancellationToken ct
  19. ) => {
  20. if (!httpContext.Request.HasFormContentType)
  21. {
  22. return CustomResults.Problem(SharedKernel.Results.Result.Failure(SharedKernel.Results.Error.Problem("OAuth.InvalidRequest", "Content-Type must be application/x-www-form-urlencoded.")));
  23. }
  24. var form = await httpContext.Request.ReadFormAsync(ct);
  25. string? clientID = form["client_id"];
  26. string? clientSecret = form["client_secret"];
  27. // HTTP Basic auth fallback (RFC 6749 §2.3.1)
  28. if (string.IsNullOrEmpty(clientID))
  29. {
  30. var auth = httpContext.Request.Headers.Authorization.ToString();
  31. if (auth.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
  32. {
  33. try
  34. {
  35. var base64 = auth["Basic ".Length..];
  36. var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(base64));
  37. var sep = decoded.IndexOf(':');
  38. if (sep > 0)
  39. {
  40. clientID = decoded[..sep];
  41. clientSecret = decoded[(sep + 1)..];
  42. }
  43. }
  44. catch (FormatException)
  45. {
  46. // ignore — invalid base64 falls through to validation
  47. }
  48. }
  49. }
  50. var command = new Application.Features.Api.OAuth.IssueToken.Command(
  51. form["grant_type"].ToString(),
  52. clientID,
  53. clientSecret,
  54. form["scope"].ToString()
  55. );
  56. var result = await sender.Send(command, ct);
  57. return result.Match(
  58. data => Results.Ok(data),
  59. CustomResults.Problem
  60. );
  61. })
  62. .WithTags("OAuth")
  63. .WithGroupName("public")
  64. .AllowAnonymous();
  65. }
  66. }