303 lines
12 KiB
C#
303 lines
12 KiB
C#
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<string>? TokenEndpointAuthMethodsSupported { get; set; }
|
|
|
|
[JsonPropertyName("token_endpoint_auth_signing_alg_values_supported")]
|
|
public IList<string>? 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<string>? ScopesSupported { get; set; }
|
|
|
|
[JsonPropertyName("response_types_supported")]
|
|
public IList<string>? ResponseTypesSupported { get; set; }
|
|
|
|
[JsonPropertyName("acr_values_supported")]
|
|
public IList<string>? AcrValuesSupported { get; set; }
|
|
|
|
[JsonPropertyName("subject_types_supported")]
|
|
public IList<string>? SubjectTypesSupported { get; set; }
|
|
|
|
[JsonPropertyName("userinfo_signing_alg_values_supported")]
|
|
public IList<string>? UserinfoSigningAlgValuesSupported { get; set; }
|
|
|
|
[JsonPropertyName("userinfo_encryption_alg_values_supported")]
|
|
public IList<string>? UserinfoEncryptionAlgValuesSupported { get; set; }
|
|
|
|
[JsonPropertyName("userinfo_encryption_enc_values_supported")]
|
|
public IList<string>? UserinfoEncryptionEncValuesSupported { get; set; }
|
|
|
|
[JsonPropertyName("id_token_signing_alg_values_supported")]
|
|
public IList<string>? IdTokenSigningAlgValuesSupported { get; set; }
|
|
|
|
[JsonPropertyName("id_token_encryption_alg_values_supported")]
|
|
public IList<string>? IdTokenEncryptionAlgValuesSupported { get; set; }
|
|
|
|
[JsonPropertyName("id_token_encryption_enc_values_supported")]
|
|
public IList<string>? IdTokenEncryptionEncValuesSupported { get; set; }
|
|
|
|
[JsonPropertyName("request_object_signing_alg_values_supported")]
|
|
public IList<string>? RequestObjectSigningAlgValuesSupported { get; set; }
|
|
|
|
[JsonPropertyName("display_values_supported")]
|
|
public IList<string>? DisplayValuesSupported { get; set; }
|
|
|
|
[JsonPropertyName("claim_types_supported")]
|
|
public IList<string>? ClaimTypesSupported { get; set; }
|
|
|
|
[JsonPropertyName("claims_supported")]
|
|
public IList<string>? 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<string>? 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<IResult> 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<IResult> GetTokenByCode(
|
|
HttpContext context,
|
|
ILogger<OpenAuthEndpoints> 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"]
|
|
};
|
|
}
|
|
}
|
|
}
|