Handler.cs 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. using System.Security.Cryptography;
  2. using System.Text;
  3. using Application.Abstractions.Authentication;
  4. using Application.Abstractions.Data;
  5. using Application.Abstractions.Messaging;
  6. using Domain.Entities.Developers.ValueObject;
  7. using Microsoft.EntityFrameworkCore;
  8. using SharedKernel.Results;
  9. namespace Application.Features.Api.OAuth.IssueToken;
  10. internal sealed class Handler(
  11. IAppDbContext db,
  12. IJwtTokenProvider jwtTokenProvider
  13. ) : ICommandHandler<Command, Result<Response>>
  14. {
  15. private const int AccessTokenTtlSeconds = 3600;
  16. public async Task<Result<Response>> Handle(Command request, CancellationToken ct)
  17. {
  18. if (!string.Equals(request.GrantType, "client_credentials", StringComparison.Ordinal))
  19. {
  20. return Result.Failure<Response>(Error.Problem("OAuth.UnsupportedGrant", "Only grant_type=client_credentials is supported."));
  21. }
  22. if (string.IsNullOrWhiteSpace(request.ClientID) || string.IsNullOrWhiteSpace(request.ClientSecret))
  23. {
  24. return Result.Failure<Response>(Error.Problem("OAuth.InvalidClient", "client_id and client_secret are required."));
  25. }
  26. var credential = await db.ApiCredential
  27. .AsTracking()
  28. .Include(c => c.Application)
  29. .FirstOrDefaultAsync(c => c.ClientID == request.ClientID, ct);
  30. if (credential is null || !credential.IsActive)
  31. {
  32. return Result.Failure<Response>(Error.Unauthorized("OAuth.InvalidCredentials", "Invalid client credentials."));
  33. }
  34. var secretHash = HashSecret(request.ClientSecret);
  35. if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(secretHash), Encoding.UTF8.GetBytes(credential.SecretHash)))
  36. {
  37. return Result.Failure<Response>(Error.Unauthorized("OAuth.InvalidCredentials", "Invalid client credentials."));
  38. }
  39. var app = credential.Application;
  40. if (app is null || app.Status != ApplicationStatus.Active)
  41. {
  42. return Result.Failure<Response>(Error.Problem("OAuth.AppNotActive", "Application is not active."));
  43. }
  44. var grantedScopes = await db.ApiApplicationScope
  45. .Where(c => c.ApplicationID == app.ID)
  46. .Select(c => c.Scope)
  47. .ToListAsync(ct);
  48. var requestedScopes = (request.Scope ?? string.Empty)
  49. .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  50. var allowedScopes = requestedScopes.Length == 0
  51. ? grantedScopes
  52. : requestedScopes.Where(s => grantedScopes.Contains(s)).ToList();
  53. if (requestedScopes.Length > 0 && allowedScopes.Count == 0)
  54. {
  55. return Result.Failure<Response>(Error.Problem("OAuth.InvalidScope", "Requested scopes are not granted."));
  56. }
  57. credential.TouchLastUsed();
  58. await db.SaveChangesAsync(ct);
  59. var ttl = TimeSpan.FromSeconds(AccessTokenTtlSeconds);
  60. var accessToken = jwtTokenProvider.CreateOAuth2AccessToken(
  61. app.ID,
  62. credential.ClientID,
  63. app.OwnerMemberID,
  64. allowedScopes,
  65. ttl
  66. );
  67. return Result.Success(new Response(
  68. accessToken,
  69. "Bearer",
  70. AccessTokenTtlSeconds,
  71. string.Join(' ', allowedScopes)
  72. ));
  73. }
  74. private static string HashSecret(string secret)
  75. {
  76. var bytes = Encoding.UTF8.GetBytes(secret);
  77. var hash = SHA256.HashData(bytes);
  78. return Convert.ToHexString(hash).ToLowerInvariant();
  79. }
  80. }