From 673cb48b7593b709159c700135a3614de832319e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 17 Apr 2022 21:04:54 +0200 Subject: [PATCH] Finishing Login/Register handling --- .../Models/Utils/Constants.cs | 13 +- .../AuthenticationModule.cs | 4 +- .../Controllers/ITokenController.cs | 49 ++-- .../Kyoo.Authentication.csproj | 2 +- .../Models/DTO/RegisterRequest.cs | 3 +- src/Kyoo.Authentication/Models/JwtToken.cs | 59 ++--- src/Kyoo.Authentication/Views/AccountApi.cs | 212 ------------------ src/Kyoo.Authentication/Views/AuthApi.cs | 174 ++++++++++++++ 8 files changed, 243 insertions(+), 273 deletions(-) delete mode 100644 src/Kyoo.Authentication/Views/AccountApi.cs create mode 100644 src/Kyoo.Authentication/Views/AuthApi.cs diff --git a/src/Kyoo.Abstractions/Models/Utils/Constants.cs b/src/Kyoo.Abstractions/Models/Utils/Constants.cs index 7f857df1..1640d779 100644 --- a/src/Kyoo.Abstractions/Models/Utils/Constants.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Constants.cs @@ -31,25 +31,30 @@ namespace Kyoo.Abstractions.Models.Utils /// public const int AlternativeRoute = 1; + /// + /// A group name for . It should be used for endpoints used by users. + /// + public const string UsersGroup = "0:Users"; + /// /// A group name for . It should be used for main resources of kyoo. /// - public const string ResourcesGroup = "0:Resources"; + public const string ResourcesGroup = "1:Resources"; /// /// A group name for . /// It should be used for sub resources of kyoo that help define the main resources. /// - public const string MetadataGroup = "1:Metadata"; + public const string MetadataGroup = "2:Metadata"; /// /// A group name for . It should be used for endpoints useful for playback. /// - public const string WatchGroup = "2:Watch"; + public const string WatchGroup = "3:Watch"; /// /// A group name for . It should be used for endpoints used by admins. /// - public const string AdminGroup = "3:Admin"; + public const string AdminGroup = "4:Admin"; } } diff --git a/src/Kyoo.Authentication/AuthenticationModule.cs b/src/Kyoo.Authentication/AuthenticationModule.cs index 8b9d7e62..0b87af6d 100644 --- a/src/Kyoo.Authentication/AuthenticationModule.cs +++ b/src/Kyoo.Authentication/AuthenticationModule.cs @@ -47,7 +47,7 @@ namespace Kyoo.Authentication public string Name => "Authentication"; /// - public string Description => "Enable an authentication/permission system for Kyoo (via Jwt or ApKeys)."; + public string Description => "Enable an authentication/permission system for Kyoo (via Jwt or ApiKeys)."; /// public Dictionary Configuration => new() @@ -62,7 +62,7 @@ namespace Kyoo.Authentication private readonly IConfiguration _configuration; /// - /// Create a new authentication module instance and use the given configuration and environment. + /// Create a new authentication module instance and use the given configuration. /// /// The configuration to use public AuthenticationModule(IConfiguration configuration) diff --git a/src/Kyoo.Authentication/Controllers/ITokenController.cs b/src/Kyoo.Authentication/Controllers/ITokenController.cs index 1bb29c03..f0ae7670 100644 --- a/src/Kyoo.Authentication/Controllers/ITokenController.cs +++ b/src/Kyoo.Authentication/Controllers/ITokenController.cs @@ -22,33 +22,34 @@ using JetBrains.Annotations; using Kyoo.Abstractions.Models; using Microsoft.IdentityModel.Tokens; -namespace Kyoo.Authentication; - -/// -/// The service that controls jwt creation and validation. -/// -public interface ITokenController +namespace Kyoo.Authentication { /// - /// Create a new access token for the given user. + /// The service that controls jwt creation and validation. /// - /// The user to create a token for. - /// When this token will expire. - /// A new, valid access token. - string CreateAccessToken([NotNull] User user, out TimeSpan expireIn); + public interface ITokenController + { + /// + /// Create a new access token for the given user. + /// + /// The user to create a token for. + /// When this token will expire. + /// A new, valid access token. + string CreateAccessToken([NotNull] User user, out TimeSpan expireIn); - /// - /// Create a new refresh token for the given user. - /// - /// The user to create a token for. - /// A new, valid refresh token. - Task CreateRefreshToken([NotNull] User user); + /// + /// Create a new refresh token for the given user. + /// + /// The user to create a token for. + /// A new, valid refresh token. + Task CreateRefreshToken([NotNull] User user); - /// - /// Check if the given refresh token is valid and if it is, retrieve the id of the user this token belongs to. - /// - /// The refresh token to validate. - /// The given refresh token is not valid. - /// The id of the token's user. - int GetRefreshTokenUserID(string refreshToken); + /// + /// Check if the given refresh token is valid and if it is, retrieve the id of the user this token belongs to. + /// + /// The refresh token to validate. + /// The given refresh token is not valid. + /// The id of the token's user. + int GetRefreshTokenUserID(string refreshToken); + } } diff --git a/src/Kyoo.Authentication/Kyoo.Authentication.csproj b/src/Kyoo.Authentication/Kyoo.Authentication.csproj index bf1ebbef..0c3f072d 100644 --- a/src/Kyoo.Authentication/Kyoo.Authentication.csproj +++ b/src/Kyoo.Authentication/Kyoo.Authentication.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Kyoo.Authentication/Models/DTO/RegisterRequest.cs b/src/Kyoo.Authentication/Models/DTO/RegisterRequest.cs index 776ba2a7..59cf66b1 100644 --- a/src/Kyoo.Authentication/Models/DTO/RegisterRequest.cs +++ b/src/Kyoo.Authentication/Models/DTO/RegisterRequest.cs @@ -20,6 +20,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Kyoo.Abstractions.Models; using Kyoo.Utils; +using BCryptNet = BCrypt.Net.BCrypt; namespace Kyoo.Authentication.Models.DTO { @@ -56,7 +57,7 @@ namespace Kyoo.Authentication.Models.DTO { Slug = Utility.ToSlug(Username), Username = Username, - Password = Password, + Password = BCryptNet.HashPassword(Password), Email = Email, ExtraData = new Dictionary() }; diff --git a/src/Kyoo.Authentication/Models/JwtToken.cs b/src/Kyoo.Authentication/Models/JwtToken.cs index e0676203..5e885a17 100644 --- a/src/Kyoo.Authentication/Models/JwtToken.cs +++ b/src/Kyoo.Authentication/Models/JwtToken.cs @@ -20,39 +20,40 @@ using System; using System.Text.Json.Serialization; using Newtonsoft.Json; -namespace Kyoo.Authentication; - -/// -/// A container representing the response of a login or token refresh. -/// -public class JwtToken +namespace Kyoo.Authentication { /// - /// The type of this token (always a Bearer). + /// A container representing the response of a login or token refresh. /// - [JsonProperty("token_token")] - [JsonPropertyName("token_type")] - public string TokenType => "Bearer";&é"bbbbR" + public class JwtToken + { + /// + /// The type of this token (always a Bearer). + /// + [JsonProperty("token_token")] + [JsonPropertyName("token_type")] + public string TokenType => "Bearer"; - /// - /// The access token used to authorize requests. - /// - [JsonProperty("access_token")] - [JsonPropertyName("access_token")] - public string AccessToken { get; set; }p + /// + /// The access token used to authorize requests. + /// + [JsonProperty("access_token")] + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } - /// - /// The refresh token used to retrieve a new access/refresh token when the access token has expired. - /// - [JsonProperty("refresh_token")] - [JsonPropertyName("refresh_token")] - public string RefreshToken { get; set; } + /// + /// The refresh token used to retrieve a new access/refresh token when the access token has expired. + /// + [JsonProperty("refresh_token")] + [JsonPropertyName("refresh_token")] + public string RefreshToken { get; set; } - /// - /// The date when the access token will expire. After this date, the refresh token should be used to retrieve. - /// a new token.cs - /// - [JsonProperty("expire_in")] - [JsonPropertyName("expire_in")] - public TimeSpan ExpireIn { get; set; } + /// + /// When the access token will expire. After this tume, the refresh token should be used to retrieve. + /// a new token.cs + /// + [JsonProperty("expire_in")] + [JsonPropertyName("expire_in")] + public TimeSpan ExpireIn { get; set; } + } } diff --git a/src/Kyoo.Authentication/Views/AccountApi.cs b/src/Kyoo.Authentication/Views/AccountApi.cs deleted file mode 100644 index 2957c73f..00000000 --- a/src/Kyoo.Authentication/Views/AccountApi.cs +++ /dev/null @@ -1,212 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Kyoo.Authentication; -using Kyoo.Authentication.Models.DTO; -using Kyoo.Utils; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using BCryptNet = BCrypt.Net.BCrypt; - -namespace Amadeus.Server.Views.Auth; - -/// -/// Sign in, Sign up or refresh tokens. -/// -[ApiController] -[Route("auth")] -public class AuthView : ControllerBase -{ - /// - /// The repository used to check if the user exists. - /// - private readonly IUserRepository _users; - - /// - /// The token generator. - /// - private readonly ITokenController _token; - - /// - /// Create a new . - /// - /// The repository used to check if the user exists. - /// The token generator. - public AuthView(IUserRepository users, ITokenController token) - { - _users = users; - _token = token; - } - - /// - /// Login. - /// - /// - /// Login as a user and retrieve an access and a refresh token. - /// - /// The body of the request. - /// A new access and a refresh token. - /// The user and password does not match. - [HttpPost("login")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> Login([FromBody] LoginRequest request) - { - User user = (await _users.GetAll()).FirstOrDefault(x => x.Username == request.Username); - if (user != null && BCryptNet.Verify(request.Password, user.Password)) - { - return new JwtToken - { - AccessToken = _token.CreateAccessToken(user, out TimeSpan expireDate), - RefreshToken = await _token.CreateRefreshToken(user), - ExpireIn = expireDate - }; - } - return BadRequest(new { Message = "The user and password does not match." }); - } - - /// - /// Register. - /// - /// - /// Register a new user and get a new access/refresh token for this new user. - /// - /// The body of the request. - /// A new access and a refresh token. - /// The request is invalid. - /// A user already exists with this username or email address. - [HttpPost("register")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task> Register([FromBody] RegisterRequest request) - { - User user = request.ToUser(); - user.Password = BCryptNet.HashPassword(request.Password); - try - { - await _users.Create(user); - } - catch (DuplicateField) - { - return Conflict(new { Message = "A user already exists with this username." }); - } - - - return new JwtToken - { - AccessToken = _token.CreateAccessToken(user, out TimeSpan expireDate), - RefreshToken = await _token.CreateRefreshToken(user), - ExpireIn = expireDate - }; - } - - /// - /// Refresh a token. - /// - /// - /// Refresh an access token using the given refresh token. A new access and refresh token are generated. - /// The old refresh token should not be used anymore. - /// - /// A valid refresh token. - /// A new access and refresh token. - /// The given refresh token is invalid. - [HttpGet("refresh")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> Refresh([FromQuery] string token) - { - try - { - int userId = _token.GetRefreshTokenUserID(token); - User user = await _users.GetById(userId); - return new JwtToken - { - AccessToken = _token.CreateAccessToken(user, out TimeSpan expireDate), - RefreshToken = await _token.CreateRefreshToken(user), - ExpireIn = expireDate - }; - } - catch (ElementNotFound) - { - return BadRequest(new { Message = "Invalid refresh token." }); - } - catch (SecurityTokenException ex) - { - return BadRequest(new { ex.Message }); - } - } - - [HttpGet("anilist")] - [ProducesResponseType(StatusCodes.Status302Found)] - public IActionResult AniListLogin([FromQuery] Uri redirectUrl, [FromServices] IOptions anilist) - { - Dictionary query = new() - { - ["client_id"] = anilist.Value.ClientID, - ["redirect_uri"] = redirectUrl.ToString(), - ["response_type"] = "code" - }; - return Redirect($"https://anilist.co/api/v2/oauth/authorize{query.ToQueryString()}"); - } - - [HttpPost("link/anilist")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize] - public async Task> AniListLink([FromQuery] string code, [FromServices] AniListService anilist) - { - // TODO prevent link if someone has already linked this account. - // TODO allow unlink. - if (!int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out int userID)) - return BadRequest("Invalid access token"); - return await anilist.LinkAccount(userID, code); - } - - [HttpPost("login/anilist")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> AniListLogin([FromQuery] string code, [FromServices] AniListService anilist) - { - User user = await anilist.Login(code); - return new JwtToken - { - AccessToken = _token.CreateAccessToken(user, out TimeSpan expireIn), - RefreshToken = await _token.CreateRefreshToken(user), - ExpireIn = expireIn - }; - } - - [HttpGet("me")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetMe() - { - if (!int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out int userID)) - return BadRequest("Invalid access token"); - return await _users.GetById(userID); - } -} diff --git a/src/Kyoo.Authentication/Views/AuthApi.cs b/src/Kyoo.Authentication/Views/AuthApi.cs new file mode 100644 index 00000000..8c2bb376 --- /dev/null +++ b/src/Kyoo.Authentication/Views/AuthApi.cs @@ -0,0 +1,174 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models; +using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Abstractions.Models.Exceptions; +using Kyoo.Abstractions.Models.Utils; +using Kyoo.Authentication.Models.DTO; +using Kyoo.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using static Kyoo.Abstractions.Models.Utils.Constants; +using BCryptNet = BCrypt.Net.BCrypt; + +namespace Kyoo.Authentication.Views +{ + /// + /// Sign in, Sign up or refresh tokens. + /// + [ApiController] + [Route("api/auth")] + [ApiDefinition("Authentication", Group = UsersGroup)] + public class AuthApi : ControllerBase + { + /// + /// The repository to handle users. + /// + private readonly IUserRepository _users; + + /// + /// The token generator. + /// + private readonly ITokenController _token; + + /// + /// Create a new . + /// + /// The repository used to check if the user exists. + /// The token generator. + public AuthApi(IUserRepository users, ITokenController token) + { + _users = users; + _token = token; + } + + /// + /// Login. + /// + /// + /// Login as a user and retrieve an access and a refresh token. + /// + /// The body of the request. + /// A new access and a refresh token. + /// The user and password does not match. + [HttpPost("login")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + public async Task> Login([FromBody] LoginRequest request) + { + User user = await _users.GetOrDefault(x => x.Username == request.Username); + if (user == null || !BCryptNet.Verify(request.Password, user.Password)) + return BadRequest(new RequestError("The user and password does not match.")); + + return new JwtToken + { + AccessToken = _token.CreateAccessToken(user, out TimeSpan expireIn), + RefreshToken = await _token.CreateRefreshToken(user), + ExpireIn = expireIn + }; + } + + /// + /// Register. + /// + /// + /// Register a new user and get a new access/refresh token for this new user. + /// + /// The body of the request. + /// A new access and a refresh token. + /// The request is invalid. + /// A user already exists with this username or email address. + [HttpPost("register")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(RequestError))] + public async Task> Register([FromBody] RegisterRequest request) + { + User user = request.ToUser(); + try + { + await _users.Create(user); + } + catch (DuplicatedItemException) + { + return Conflict(new RequestError("A user already exists with this username.")); + } + + return new JwtToken + { + AccessToken = _token.CreateAccessToken(user, out TimeSpan expireIn), + RefreshToken = await _token.CreateRefreshToken(user), + ExpireIn = expireIn + }; + } + + /// + /// Refresh a token. + /// + /// + /// Refresh an access token using the given refresh token. A new access and refresh token are generated. + /// + /// A valid refresh token. + /// A new access and refresh token. + /// The given refresh token is invalid. + [HttpGet("refresh")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + public async Task> Refresh([FromQuery] string token) + { + try + { + int userId = _token.GetRefreshTokenUserID(token); + User user = await _users.Get(userId); + return new JwtToken + { + AccessToken = _token.CreateAccessToken(user, out TimeSpan expireIn), + RefreshToken = await _token.CreateRefreshToken(user), + ExpireIn = expireIn + }; + } + catch (ItemNotFoundException) + { + return BadRequest(new RequestError("Invalid refresh token.")); + } + catch (SecurityTokenException ex) + { + return BadRequest(new RequestError(ex.Message)); + } + } + + [HttpGet("me")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetMe() + { + if (!int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out int userID)) + return BadRequest("Invalid access token"); + return await _users.GetById(userID); + } + } +}