finally implemented oauth 2.0

This commit is contained in:
Vasya Ryzhkoff 2024-07-27 12:21:46 +07:00
parent d332cd0d96
commit 37146568a8
10 changed files with 260 additions and 24 deletions

View File

@ -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);

View File

@ -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!;
}
}

View File

@ -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
@ -141,10 +154,11 @@ namespace Yuna.Website.Server.API
} }
public IResult LoginViaOauth( public IResult LoginViaOauth(
[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"]
};
} }
} }
} }

View File

@ -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);
} }
} }

View File

@ -43,5 +43,10 @@ namespace Yuna.Website.Server.Services.OpenAuthService
return result; return result;
} }
public UserBinding? GetByCode(string code)
{
return _userBindingsRepository.GetUserBinding(code);
}
} }
} }

View File

@ -191,8 +191,8 @@ namespace Yuna.Website.Server
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
builder.Services.AddCors(); builder.Services.AddCors();
//builder.Services.AddAutoMapper(typeof(ApplicationProfile)); //builder.Services.AddAutoMapper(typeof(ApplicationProfile));
} }
@ -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
@ -213,12 +220,12 @@ namespace Yuna.Website.Server
app.UseDefaultFiles(); app.UseDefaultFiles();
app.UseStaticFiles(); app.UseStaticFiles();
app.Use(async (context, next) => app.Use(async (context, next) =>
{ {
var isLoggedIn = context.User?.Identity?.IsAuthenticated ?? false; var isLoggedIn = context.User?.Identity?.IsAuthenticated ?? false;
if(context.Request.Path.StartsWithSegments("/login") && isLoggedIn) if (context.Request.Path.StartsWithSegments("/login") && isLoggedIn)
{ {
context.Response.Redirect("/"); context.Response.Redirect("/");
return; return;

View File

@ -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 };
}
}
} }

View File

@ -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;

View File

@ -6,4 +6,11 @@ export interface ILoginResult {
export interface ILoginRequest { export interface ILoginRequest {
password: string; password: string;
username: string; username: string;
} }
export interface IOauthQueryParams {
redirectUri: string;
state: string;
}
export interface IOauthLoginRequest extends IOauthQueryParams, ILoginRequest { }

View File

@ -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 => {