using System.Security.Cryptography; using System.Text; using Application.Abstractions.Authentication; using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Domain.Entities.Developers.ValueObject; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; namespace Application.Features.Api.OAuth.IssueToken; internal sealed class Handler( IAppDbContext db, IJwtTokenProvider jwtTokenProvider ) : ICommandHandler> { private const int AccessTokenTtlSeconds = 3600; public async Task> Handle(Command request, CancellationToken ct) { if (!string.Equals(request.GrantType, "client_credentials", StringComparison.Ordinal)) { return Result.Failure(Error.Problem("OAuth.UnsupportedGrant", "Only grant_type=client_credentials is supported.")); } if (string.IsNullOrWhiteSpace(request.ClientID) || string.IsNullOrWhiteSpace(request.ClientSecret)) { return Result.Failure(Error.Problem("OAuth.InvalidClient", "client_id and client_secret are required.")); } var credential = await db.ApiCredential .AsTracking() .Include(c => c.Application) .FirstOrDefaultAsync(c => c.ClientID == request.ClientID, ct); if (credential is null || !credential.IsActive) { return Result.Failure(Error.Unauthorized("OAuth.InvalidCredentials", "Invalid client credentials.")); } var secretHash = HashSecret(request.ClientSecret); if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(secretHash), Encoding.UTF8.GetBytes(credential.SecretHash))) { return Result.Failure(Error.Unauthorized("OAuth.InvalidCredentials", "Invalid client credentials.")); } var app = credential.Application; if (app is null || app.Status != ApplicationStatus.Active) { return Result.Failure(Error.Problem("OAuth.AppNotActive", "Application is not active.")); } var grantedScopes = await db.ApiApplicationScope .Where(c => c.ApplicationID == app.ID) .Select(c => c.Scope) .ToListAsync(ct); var requestedScopes = (request.Scope ?? string.Empty) .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var allowedScopes = requestedScopes.Length == 0 ? grantedScopes : requestedScopes.Where(s => grantedScopes.Contains(s)).ToList(); if (requestedScopes.Length > 0 && allowedScopes.Count == 0) { return Result.Failure(Error.Problem("OAuth.InvalidScope", "Requested scopes are not granted.")); } credential.TouchLastUsed(); await db.SaveChangesAsync(ct); var ttl = TimeSpan.FromSeconds(AccessTokenTtlSeconds); var accessToken = jwtTokenProvider.CreateOAuth2AccessToken( app.ID, credential.ClientID, app.OwnerMemberID, allowedScopes, ttl ); return Result.Success(new Response( accessToken, "Bearer", AccessTokenTtlSeconds, string.Join(' ', allowedScopes) )); } private static string HashSecret(string secret) { var bytes = Encoding.UTF8.GetBytes(secret); var hash = SHA256.HashData(bytes); return Convert.ToHexString(hash).ToLowerInvariant(); } }