finally implemented oauth 2.0
This commit is contained in:
parent
d332cd0d96
commit
37146568a8
|
@ -7,6 +7,7 @@ using System.Text.Json.Serialization;
|
||||||
using Yuna.Website.Server.Services.TokenService;
|
using Yuna.Website.Server.Services.TokenService;
|
||||||
using Yuna.Website.Server.Model;
|
using Yuna.Website.Server.Model;
|
||||||
using Yuna.Website.Server.Services.UserService;
|
using Yuna.Website.Server.Services.UserService;
|
||||||
|
using Yuna.Website.Server.API.DTO;
|
||||||
|
|
||||||
namespace Yuna.Website.Server.API
|
namespace Yuna.Website.Server.API
|
||||||
{
|
{
|
||||||
|
@ -27,14 +28,7 @@ namespace Yuna.Website.Server.API
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LoginRequest
|
|
||||||
{
|
|
||||||
[JsonPropertyName("password")]
|
|
||||||
public string RawPassword { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("username")]
|
|
||||||
public string UserName { get; set; } = null!;
|
|
||||||
}
|
|
||||||
public async Task<IResult> Login(HttpContext context, [FromBody] LoginRequest request, IUserService userService, ITokenService tokenService)
|
public async Task<IResult> Login(HttpContext context, [FromBody] LoginRequest request, IUserService userService, ITokenService tokenService)
|
||||||
{
|
{
|
||||||
var userFromDb = await userService.GetByUsername(request.UserName);
|
var userFromDb = await userService.GetByUsername(request.UserName);
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Yuna.Website.Server.API.DTO
|
||||||
|
{
|
||||||
|
public class LoginRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("password")]
|
||||||
|
public string RawPassword { get; set; } = null!;
|
||||||
|
|
||||||
|
[JsonPropertyName("username")]
|
||||||
|
public string UserName { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,11 @@
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Yuna.Website.Server.API.DTO;
|
||||||
using Yuna.Website.Server.Infrastructure;
|
using Yuna.Website.Server.Infrastructure;
|
||||||
using Yuna.Website.Server.Services.OpenAuthService;
|
using Yuna.Website.Server.Services.OpenAuthService;
|
||||||
|
using Yuna.Website.Server.Services.UserService;
|
||||||
|
using Yuna.Website.Server.Storage.Repositories.UserBindings;
|
||||||
|
|
||||||
namespace Yuna.Website.Server.API
|
namespace Yuna.Website.Server.API
|
||||||
{
|
{
|
||||||
|
@ -9,11 +13,20 @@ namespace Yuna.Website.Server.API
|
||||||
{
|
{
|
||||||
public void Define(WebApplication app)
|
public void Define(WebApplication app)
|
||||||
{
|
{
|
||||||
app.MapGet("~/.well-known/openid-configuration", GetConfiguration);
|
app.MapGet("~/.well-known/openid-configuration", GetConfiguration)
|
||||||
|
.WithTags("oauth");
|
||||||
|
|
||||||
app.MapMethods("/v1.0", ["HEAD"], Ping);
|
app.MapMethods("/v1.0", ["HEAD"], Ping)
|
||||||
|
.WithTags("oauth");
|
||||||
|
|
||||||
app.MapGet("api/oauth/login", LoginViaOauth);
|
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
|
public class DiscoveryResponse
|
||||||
|
@ -144,7 +157,8 @@ namespace Yuna.Website.Server.API
|
||||||
[FromQuery] string state,
|
[FromQuery] string state,
|
||||||
[FromQuery] string redirect_uri,
|
[FromQuery] string redirect_uri,
|
||||||
[FromQuery] string response_type,
|
[FromQuery] string response_type,
|
||||||
[FromQuery]string client_id, HttpContext context,
|
[FromQuery] string client_id,
|
||||||
|
HttpContext context,
|
||||||
IOpenAuthService openAuthService)
|
IOpenAuthService openAuthService)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -153,7 +167,136 @@ namespace Yuna.Website.Server.API
|
||||||
return Results.Unauthorized();
|
return Results.Unauthorized();
|
||||||
|
|
||||||
//TODO LOGIN PAGE URL IN SETTINGS
|
//TODO LOGIN PAGE URL IN SETTINGS
|
||||||
return Results.Redirect($"https://localhost:5173/login?redirect_to={redirect_uri}");
|
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"]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,6 @@ namespace Yuna.Website.Server.Services.OpenAuthService
|
||||||
{
|
{
|
||||||
public bool ValidateLoginRequest(string responseType, string clientId, string host);
|
public bool ValidateLoginRequest(string responseType, string clientId, string host);
|
||||||
public UserBinding CreateBinding(User user);
|
public UserBinding CreateBinding(User user);
|
||||||
|
public UserBinding? GetByCode(string code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,5 +43,10 @@ namespace Yuna.Website.Server.Services.OpenAuthService
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UserBinding? GetByCode(string code)
|
||||||
|
{
|
||||||
|
return _userBindingsRepository.GetUserBinding(code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -201,7 +201,14 @@ namespace Yuna.Website.Server
|
||||||
{
|
{
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseCors(x => x.WithOrigins("https://localhost:5173", "http://localhost:5173").AllowAnyMethod().AllowAnyHeader().AllowCredentials());
|
app.UseCors(x =>
|
||||||
|
x.WithOrigins("https://localhost:5173", "http://localhost:5173", "https://192.168.1.2:5227")
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowCredentials()
|
||||||
|
.SetIsOriginAllowed(origin => true) // Разрешить все источники
|
||||||
|
.WithExposedHeaders("Location")); // Разрешить заголовок Location)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
|
||||||
import { api } from "../../services/Api";
|
import { api } from "../../services/Api";
|
||||||
import { ILoginRequest, ILoginResult } from "./types";
|
import { ILoginRequest, ILoginResult, IOauthLoginRequest } from "./types";
|
||||||
|
|
||||||
|
interface IOauthLoginResult extends ILoginResult {
|
||||||
|
href: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export class LoginPageService {
|
export class LoginPageService {
|
||||||
public static async login(request: ILoginRequest): Promise<ILoginResult> {
|
public static async login(request: ILoginRequest): Promise<ILoginResult> {
|
||||||
|
@ -18,4 +21,20 @@ export class LoginPageService {
|
||||||
return { loggedIn: false, isError: true };
|
return { loggedIn: false, isError: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static async loginOauth(request: IOauthLoginRequest): Promise<IOauthLoginResult> {
|
||||||
|
try {
|
||||||
|
const response = await api.post<string>("/oauth/auth", request);
|
||||||
|
if (response.status === 200) return { loggedIn: true, isError: false, href: response.data };
|
||||||
|
|
||||||
|
return { loggedIn: false, isError: false, href: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (error) {
|
||||||
|
console.error("Error: ", error);
|
||||||
|
return { loggedIn: false, isError: true, href: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { makeAutoObservable } from "mobx";
|
||||||
import { LoginPageService } from "./LoginPageService";
|
import { LoginPageService } from "./LoginPageService";
|
||||||
import { createStandaloneToast } from "@chakra-ui/react";
|
import { createStandaloneToast } from "@chakra-ui/react";
|
||||||
import { error } from "../../utils/ToastHelper";
|
import { error } from "../../utils/ToastHelper";
|
||||||
|
import { IOauthQueryParams } from "./types";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,6 +12,17 @@ export class LoginPageStore {
|
||||||
password: string = "";
|
password: string = "";
|
||||||
isLoading: boolean = false;
|
isLoading: boolean = false;
|
||||||
|
|
||||||
|
private getQueryParams(): IOauthQueryParams | null {
|
||||||
|
const url = window.location.href;
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const params = new URLSearchParams(urlObj.search);
|
||||||
|
|
||||||
|
const redirectUri: string | null = params.get('redirect_to');
|
||||||
|
const state: string | null = params.get('state');
|
||||||
|
|
||||||
|
if (state && redirectUri) return { redirectUri: redirectUri, state: state }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
setLogin(value: string) {
|
setLogin(value: string) {
|
||||||
this.login = value;
|
this.login = value;
|
||||||
|
@ -22,7 +34,43 @@ export class LoginPageStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleLogin() {
|
async handleLogin() {
|
||||||
|
const oauthParams = this.getQueryParams()
|
||||||
|
|
||||||
|
if (oauthParams) await this.loginAsOauth(oauthParams);
|
||||||
|
else await this.loginSimpleWay()
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginAsOauth(oauthParams: IOauthQueryParams) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
|
const result = await LoginPageService
|
||||||
|
.loginOauth({
|
||||||
|
password: this.password,
|
||||||
|
username: this.login,
|
||||||
|
redirectUri: oauthParams.redirectUri,
|
||||||
|
state: oauthParams.state
|
||||||
|
});
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
if (result?.isError) {
|
||||||
|
error("Ошибка", "Запрос не был выполнен")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result && !result.loggedIn) {
|
||||||
|
error("Неверный логин или пароль", "Проверьте еще раз")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.replace(result.href!);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async loginSimpleWay() {
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
const result = await LoginPageService.login({ password: this.password, username: this.login })
|
const result = await LoginPageService.login({ password: this.password, username: this.login })
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|
||||||
|
|
|
@ -7,3 +7,10 @@ export interface ILoginRequest {
|
||||||
password: string;
|
password: string;
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IOauthQueryParams {
|
||||||
|
redirectUri: string;
|
||||||
|
state: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOauthLoginRequest extends IOauthQueryParams, ILoginRequest { }
|
|
@ -4,7 +4,7 @@ export const api = axios.create({
|
||||||
baseURL: "https://192.168.1.2:5227/api/",
|
baseURL: "https://192.168.1.2:5227/api/",
|
||||||
validateStatus: (status) => status < 500,
|
validateStatus: (status) => status < 500,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: { 'Accept': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
});
|
});
|
||||||
|
|
||||||
api.interceptors.response.use(response => {
|
api.interceptors.response.use(response => {
|
||||||
|
|
Loading…
Reference in New Issue