using System.ComponentModel.DataAnnotations; using System.Text.Json; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; using OAuthServer.Services; namespace OAuthServer.Controllers; [ApiController] [Route("oauth")] public class OAuthController( ILogger logger, JwtService jwt, IDataProtectionProvider dataProtectionProvider ) : ControllerBase { private record Client(string ClientId, string ClientSecret, string ClientOrigin, string ClientScope); private record CodeObject(string ClientId, string RedirectUri, DateTime Expiry); private readonly Dictionary _clients = new() { {"lmao", new Client("lmao", "yeet", "http://localhost:5126", "41")}, }; [Authorize] [HttpGet("authorize")] // ReSharper disable InconsistentNaming public ActionResult Authorize( [Required, Url] string redirect_uri, string? response_type, string? client_id, string? state ) { // Check if the required fields are present if (string.IsNullOrEmpty(response_type) || string.IsNullOrEmpty(client_id) || string.IsNullOrEmpty(state)) { return Redirect($"{redirect_uri}?error=invalid_request"); } // The only supported option is "code" if (response_type != "code") { return Redirect($"{redirect_uri}?error=unsupported_response_type&state={state}"); } // Check if the client exists if (!_clients.TryGetValue(client_id, out Client? client)) { logger.LogInformation("Unknown client id"); return Redirect($"{redirect_uri}?error=unauthorized_client&state={state}"); } // Check if the origin matches the pre-registered one string origin = GetOrigin(redirect_uri); if (origin != client.ClientOrigin) { return Redirect($"{redirect_uri}?error=unauthorized_client&state={state}"); } // Turn the client info with an expiration to a opaque value using the data protection api IDataProtector protector = dataProtectionProvider.CreateProtector("oauth"); CodeObject codeObject = new CodeObject( ClientId: client_id, RedirectUri: redirect_uri, Expiry: DateTime.UtcNow.AddMinutes(5) ); string code = protector.Protect(JsonSerializer.Serialize(codeObject)); return Redirect($"{redirect_uri}?code={code}&state={state}"); } public record GenerateTokenRequest( string? grant_type, string? code, string? redirect_uri, string? client_id, string? client_secret ); [HttpPost("token")] [Consumes("application/x-www-form-urlencoded")] public ActionResult GenerateToken([FromForm] GenerateTokenRequest request) { // Check if all the required fields are present if (string.IsNullOrEmpty(request.grant_type) || string.IsNullOrEmpty(request.code) || string.IsNullOrEmpty(request.redirect_uri) || string.IsNullOrEmpty(request.client_id) || string.IsNullOrEmpty(request.client_secret)) { return BadRequest(new {error = "invalid_request"}); } // The only supported option is "authorization_code" if (request.grant_type != "authorization_code") { return BadRequest(new {error = "unsupported_grant_type"}); } // Check if the client exists if (!_clients.TryGetValue(request.client_id, out Client? client)) { logger.LogInformation("Unknown client id"); return BadRequest(new {error = "unauthorized_client"}); } // Check if the client secret matches if (request.client_secret != client.ClientSecret) { logger.LogInformation("Invalid client secret"); return BadRequest(new {error = "unauthorized_client"}); } // Retrieve client information set by the preceding authorization step IDataProtector protector = dataProtectionProvider.CreateProtector("oauth"); CodeObject? codeObject; try { codeObject = JsonSerializer.Deserialize(protector.Unprotect(request.code)); } catch (Exception) { return BadRequest(new {error = "invalid_request"}); } if (codeObject == null) { return BadRequest(new {error = "invalid_request"}); } // Check if the values are consistent if (codeObject.ClientId != request.client_id || codeObject.RedirectUri != request.redirect_uri) { return BadRequest(new {error = "invalid_request"}); } // Check the token's expiration if (DateTime.UtcNow > codeObject.Expiry) { logger.LogInformation("Expired token"); return BadRequest(new {error = "invalid_grant"}); } // Generate the auth token for the application server string token = jwt.GenerateToken("1", client.ClientId, "External", client.ClientScope); // Add http headers to prevent caching Response.Headers.Append("Cache-Control", "no-store"); Response.Headers.Append("Pragma", "no-cache"); return Ok(new { access_token = token, token_type = "bearer", scope = client.ClientScope }); } private static string GetOrigin(string url) { Uri uri = new Uri(url); return $"{uri.Scheme}://{uri.Authority}"; } }