using Microsoft.AspNetCore.Mvc; using System.Text; using System.Text.Json.Serialization; using Yuna.Website.Server.API.DTO; using Yuna.Website.Server.Infrastructure; using Yuna.Website.Server.Services.OpenAuthService; using Yuna.Website.Server.Services.UserService; using Yuna.Website.Server.Storage.Repositories.UserBindings; namespace Yuna.Website.Server.API { public class OpenAuthEndpoints { public void Define(WebApplication app) { app.MapGet("~/.well-known/openid-configuration", GetConfiguration) .WithTags("oauth"); app.MapMethods("/v1.0", ["HEAD"], Ping) .WithTags("oauth"); app.MapGet("api/oauth/login", LoginViaOauth) .WithTags("oauth"); app.MapPost("api/oauth/auth", AuthorizeViaOauth) .WithTags("oauth"); app.MapPost("/api/oauth/token", GetTokenByCode) .WithTags("oauth"); } public class DiscoveryResponse { [JsonPropertyName("issuer")] public string? Issuer { get; set; } [JsonPropertyName("authorization_endpoint")] public string? AuthorizationEndpoint { get; set; } [JsonPropertyName("token_endpoint")] public string? TokenEndpoint { get; set; } [JsonPropertyName("token_endpoint_auth_methods_supported")] public IList? TokenEndpointAuthMethodsSupported { get; set; } [JsonPropertyName("token_endpoint_auth_signing_alg_values_supported")] public IList? TokenEndpointAuthSigningAlgValuesSupported { get; set; } [JsonPropertyName("userinfo_endpoint")] public string? UserinfoEndpoint { get; set; } [JsonPropertyName("check_session_iframe")] public string? CheckSessionIframe { get; set; } [JsonPropertyName("end_session_endpoint")] public string? EndSessionEndpoint { get; set; } [JsonPropertyName("jwks_uri")] public string? JwksUri { get; set; } [JsonPropertyName("registration_endpoint")] public string? RegistrationEndpoint { get; set; } [JsonPropertyName("scopes_supported")] public IList? ScopesSupported { get; set; } [JsonPropertyName("response_types_supported")] public IList? ResponseTypesSupported { get; set; } [JsonPropertyName("acr_values_supported")] public IList? AcrValuesSupported { get; set; } [JsonPropertyName("subject_types_supported")] public IList? SubjectTypesSupported { get; set; } [JsonPropertyName("userinfo_signing_alg_values_supported")] public IList? UserinfoSigningAlgValuesSupported { get; set; } [JsonPropertyName("userinfo_encryption_alg_values_supported")] public IList? UserinfoEncryptionAlgValuesSupported { get; set; } [JsonPropertyName("userinfo_encryption_enc_values_supported")] public IList? UserinfoEncryptionEncValuesSupported { get; set; } [JsonPropertyName("id_token_signing_alg_values_supported")] public IList? IdTokenSigningAlgValuesSupported { get; set; } [JsonPropertyName("id_token_encryption_alg_values_supported")] public IList? IdTokenEncryptionAlgValuesSupported { get; set; } [JsonPropertyName("id_token_encryption_enc_values_supported")] public IList? IdTokenEncryptionEncValuesSupported { get; set; } [JsonPropertyName("request_object_signing_alg_values_supported")] public IList? RequestObjectSigningAlgValuesSupported { get; set; } [JsonPropertyName("display_values_supported")] public IList? DisplayValuesSupported { get; set; } [JsonPropertyName("claim_types_supported")] public IList? ClaimTypesSupported { get; set; } [JsonPropertyName("claims_supported")] public IList? ClaimsSupported { get; set; } [JsonPropertyName("claims_parameter_supported")] public bool? ClaimsParameterSupported { get; set; } [JsonPropertyName("service_documentation")] public string? ServiceDocumentation { get; set; } [JsonPropertyName("ui_locales_supported")] public IList? UiLocalesSupported { get; set; } } public IResult GetConfiguration() { var response = new DiscoveryResponse() { Issuer = Settings.HttpsExternalUrl, AuthorizationEndpoint = Settings.HttpsExternalUrl + "/makaka", TokenEndpoint = Settings.HttpsExternalUrl + "/token", TokenEndpointAuthMethodsSupported = ["client_secret_basic", "private_key_jwt"], TokenEndpointAuthSigningAlgValuesSupported = ["RS256", "ES256"], AcrValuesSupported = ["urn:mace:incommon:iap:silver", "urn:mace:incommon:iap:bronze"], ResponseTypesSupported = ["code", "code id_token", "id_token", "token id_token"], SubjectTypesSupported = [], UserinfoEncryptionEncValuesSupported = ["A128CBC-HS256", "A128GCM"], IdTokenSigningAlgValuesSupported = ["RS256", "ES256", "HS256"], IdTokenEncryptionAlgValuesSupported = ["RSA1_5", "A128KW"], IdTokenEncryptionEncValuesSupported = ["A128CBC-HS256", "A128GCM"], RequestObjectSigningAlgValuesSupported = ["none", "RS256", "ES256"], DisplayValuesSupported = ["page", "popup"], ClaimTypesSupported = ["normal", "distributed"], ScopesSupported = [], ClaimsSupported = [], ClaimsParameterSupported = false, ServiceDocumentation = null, UiLocalesSupported = ["ru-RU"], }; return Results.Json(response); } public IResult Ping() { return Results.Ok(); } public IResult LoginViaOauth( [FromQuery] string state, [FromQuery] string redirect_uri, [FromQuery] string response_type, [FromQuery] string client_id, HttpContext context, IOpenAuthService openAuthService) { var host = context.Request.Host; if (!openAuthService.ValidateLoginRequest(response_type, client_id, host.Value)) return Results.Unauthorized(); //TODO LOGIN PAGE URL IN SETTINGS return Results.Redirect($"https://localhost:5173/login?redirect_to={redirect_uri}&client_id={client_id}&state={state}"); } public class OauthLoginRequest : LoginRequest { [JsonPropertyName("state")] public string State { get; set; } = null!; [JsonPropertyName("redirectUri")] public string RedirectUri { get; set; } = null!; } public async Task AuthorizeViaOauth([FromBody] OauthLoginRequest request, IUserService userService, IOpenAuthService openAuthService, HttpContext context) { var user = await userService.GetByUsername(request.UserName); if (user is null) return Results.Problem(statusCode: 401, detail:"invalid user input data"); var inputHashedPassword = Encrypter.HashPassword(request.RawPassword, user.UserName); if (!inputHashedPassword.Equals(user.HashedPassword)) return Results.Problem(statusCode: 401, detail: "invalid login attempt"); var binding = openAuthService.CreateBinding(user); if(binding is null) return Results.Problem(statusCode: 401, detail: "Unable to create binding"); return Results.Ok(request.RedirectUri + $"?code={binding.Code}&state={request.State}"); } public class OauthTokenResponse { [JsonPropertyName("access_token")] public string AccessToken { get; set; } = null!; [JsonPropertyName("token_type")] public string TokenType => "Bearer"; [JsonPropertyName("expires_in")] public int ExpiresIn => int.MaxValue; } public class TokenRequest { public string Code { get; set; } public string ClientId { get; set; } = null!; public string ClientSecret { get; set; } = null!; public string GrantType { get; set; } = null!; public string RedirectUri { get; set; } = null!; } public async Task GetTokenByCode( HttpContext context, ILogger logger, IOpenAuthService openAuthService) { var request = context.Request; var sb = new StringBuilder(); // Записываем метод и URL запроса sb.AppendLine($"{request.Method} {request.Path}{request.QueryString}"); // Записываем заголовки sb.AppendLine("Headers:"); foreach (var header in request.Headers) { sb.AppendLine($"{header.Key}: {header.Value}"); } // Записываем тело запроса string bodyStr = ""; sb.AppendLine("Body:"); request.EnableBuffering(); // Для того чтобы можно было прочитать тело несколько раз using (var reader = new StreamReader(request.Body, Encoding.UTF8, leaveOpen: true)) { var body = await reader.ReadToEndAsync(); sb.AppendLine(body); bodyStr = body; request.Body.Position = 0; // Возвращаем позицию в начале потока } logger.LogInformation(sb.ToString()); var tokenRequest = ParseQueryStringToTokenRequest(bodyStr); if (tokenRequest.GrantType != "authorization_code") { return Results.BadRequest(new { error = "unsupported_grant_type" }); } if(!tokenRequest.ClientId.Equals("vasich_yandex_server")) { return Results.BadRequest(new { error = "client id invalid" }); } if(!tokenRequest.ClientSecret.Equals("vasich_molodets_sdelal_oauth")) { return Results.BadRequest(new { error = "client secret invalid" }); } var userBinding = openAuthService.GetByCode(tokenRequest.Code); if (userBinding is null) return Results.BadRequest(new { error = "Invalid binding" }); return Results.Ok(new OauthTokenResponse() { AccessToken = userBinding.AccessToken }); } private static TokenRequest ParseQueryStringToTokenRequest(string queryString) { var queryParams = queryString.Split('&') .Select(param => param.Split('=')) .ToDictionary(pair => pair[0], pair => Uri.UnescapeDataString(pair[1])); return new TokenRequest { Code = queryParams["code"], ClientId = queryParams["client_id"], ClientSecret = queryParams["client_secret"], GrantType = queryParams["grant_type"], RedirectUri = queryParams["redirect_uri"] }; } } }