mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Finishing Login/Register handling
This commit is contained in:
parent
1a534d2325
commit
673cb48b75
@ -31,25 +31,30 @@ namespace Kyoo.Abstractions.Models.Utils
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const int AlternativeRoute = 1;
|
public const int AlternativeRoute = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by users.
|
||||||
|
/// </summary>
|
||||||
|
public const string UsersGroup = "0:Users";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for main resources of kyoo.
|
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for main resources of kyoo.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string ResourcesGroup = "0:Resources";
|
public const string ResourcesGroup = "1:Resources";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A group name for <see cref="ApiDefinitionAttribute"/>.
|
/// A group name for <see cref="ApiDefinitionAttribute"/>.
|
||||||
/// It should be used for sub resources of kyoo that help define the main resources.
|
/// It should be used for sub resources of kyoo that help define the main resources.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string MetadataGroup = "1:Metadata";
|
public const string MetadataGroup = "2:Metadata";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints useful for playback.
|
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints useful for playback.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string WatchGroup = "2:Watch";
|
public const string WatchGroup = "3:Watch";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by admins.
|
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by admins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string AdminGroup = "3:Admin";
|
public const string AdminGroup = "4:Admin";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ namespace Kyoo.Authentication
|
|||||||
public string Name => "Authentication";
|
public string Name => "Authentication";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
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).";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Dictionary<string, Type> Configuration => new()
|
public Dictionary<string, Type> Configuration => new()
|
||||||
@ -62,7 +62,7 @@ namespace Kyoo.Authentication
|
|||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new authentication module instance and use the given configuration and environment.
|
/// Create a new authentication module instance and use the given configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configuration">The configuration to use</param>
|
/// <param name="configuration">The configuration to use</param>
|
||||||
public AuthenticationModule(IConfiguration configuration)
|
public AuthenticationModule(IConfiguration configuration)
|
||||||
|
@ -22,33 +22,34 @@ using JetBrains.Annotations;
|
|||||||
using Kyoo.Abstractions.Models;
|
using Kyoo.Abstractions.Models;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
namespace Kyoo.Authentication;
|
namespace Kyoo.Authentication
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The service that controls jwt creation and validation.
|
|
||||||
/// </summary>
|
|
||||||
public interface ITokenController
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new access token for the given user.
|
/// The service that controls jwt creation and validation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">The user to create a token for.</param>
|
public interface ITokenController
|
||||||
/// <param name="expireIn">When this token will expire.</param>
|
{
|
||||||
/// <returns>A new, valid access token.</returns>
|
/// <summary>
|
||||||
string CreateAccessToken([NotNull] User user, out TimeSpan expireIn);
|
/// Create a new access token for the given user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user to create a token for.</param>
|
||||||
|
/// <param name="expireIn">When this token will expire.</param>
|
||||||
|
/// <returns>A new, valid access token.</returns>
|
||||||
|
string CreateAccessToken([NotNull] User user, out TimeSpan expireIn);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new refresh token for the given user.
|
/// Create a new refresh token for the given user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">The user to create a token for.</param>
|
/// <param name="user">The user to create a token for.</param>
|
||||||
/// <returns>A new, valid refresh token.</returns>
|
/// <returns>A new, valid refresh token.</returns>
|
||||||
Task<string> CreateRefreshToken([NotNull] User user);
|
Task<string> CreateRefreshToken([NotNull] User user);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check if the given refresh token is valid and if it is, retrieve the id of the user this token belongs to.
|
/// Check if the given refresh token is valid and if it is, retrieve the id of the user this token belongs to.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="refreshToken">The refresh token to validate.</param>
|
/// <param name="refreshToken">The refresh token to validate.</param>
|
||||||
/// <exception cref="SecurityTokenException">The given refresh token is not valid.</exception>
|
/// <exception cref="SecurityTokenException">The given refresh token is not valid.</exception>
|
||||||
/// <returns>The id of the token's user.</returns>
|
/// <returns>The id of the token's user.</returns>
|
||||||
int GetRefreshTokenUserID(string refreshToken);
|
int GetRefreshTokenUserID(string refreshToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.12" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.12" />
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.2" />
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
|
|
||||||
<ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" />
|
<ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" />
|
||||||
|
@ -20,6 +20,7 @@ using System.Collections.Generic;
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Kyoo.Abstractions.Models;
|
using Kyoo.Abstractions.Models;
|
||||||
using Kyoo.Utils;
|
using Kyoo.Utils;
|
||||||
|
using BCryptNet = BCrypt.Net.BCrypt;
|
||||||
|
|
||||||
namespace Kyoo.Authentication.Models.DTO
|
namespace Kyoo.Authentication.Models.DTO
|
||||||
{
|
{
|
||||||
@ -56,7 +57,7 @@ namespace Kyoo.Authentication.Models.DTO
|
|||||||
{
|
{
|
||||||
Slug = Utility.ToSlug(Username),
|
Slug = Utility.ToSlug(Username),
|
||||||
Username = Username,
|
Username = Username,
|
||||||
Password = Password,
|
Password = BCryptNet.HashPassword(Password),
|
||||||
Email = Email,
|
Email = Email,
|
||||||
ExtraData = new Dictionary<string, string>()
|
ExtraData = new Dictionary<string, string>()
|
||||||
};
|
};
|
||||||
|
@ -20,39 +20,40 @@ using System;
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Kyoo.Authentication;
|
namespace Kyoo.Authentication
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A container representing the response of a login or token refresh.
|
|
||||||
/// </summary>
|
|
||||||
public class JwtToken
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The type of this token (always a Bearer).
|
/// A container representing the response of a login or token refresh.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("token_token")]
|
public class JwtToken
|
||||||
[JsonPropertyName("token_type")]
|
{
|
||||||
public string TokenType => "Bearer";&é"bbbbR"
|
/// <summary>
|
||||||
|
/// The type of this token (always a Bearer).
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("token_token")]
|
||||||
|
[JsonPropertyName("token_type")]
|
||||||
|
public string TokenType => "Bearer";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The access token used to authorize requests.
|
/// The access token used to authorize requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("access_token")]
|
[JsonProperty("access_token")]
|
||||||
[JsonPropertyName("access_token")]
|
[JsonPropertyName("access_token")]
|
||||||
public string AccessToken { get; set; }p
|
public string AccessToken { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The refresh token used to retrieve a new access/refresh token when the access token has expired.
|
/// The refresh token used to retrieve a new access/refresh token when the access token has expired.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("refresh_token")]
|
[JsonProperty("refresh_token")]
|
||||||
[JsonPropertyName("refresh_token")]
|
[JsonPropertyName("refresh_token")]
|
||||||
public string RefreshToken { get; set; }
|
public string RefreshToken { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The date when the access token will expire. After this date, the refresh token should be used to retrieve.
|
/// When the access token will expire. After this tume, the refresh token should be used to retrieve.
|
||||||
/// a new token.cs
|
/// a new token.cs
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("expire_in")]
|
[JsonProperty("expire_in")]
|
||||||
[JsonPropertyName("expire_in")]
|
[JsonPropertyName("expire_in")]
|
||||||
public TimeSpan ExpireIn { get; set; }
|
public TimeSpan ExpireIn { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sign in, Sign up or refresh tokens.
|
|
||||||
/// </summary>
|
|
||||||
[ApiController]
|
|
||||||
[Route("auth")]
|
|
||||||
public class AuthView : ControllerBase
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The repository used to check if the user exists.
|
|
||||||
/// </summary>
|
|
||||||
private readonly IUserRepository _users;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The token generator.
|
|
||||||
/// </summary>
|
|
||||||
private readonly ITokenController _token;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new <see cref="AuthView"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="users">The repository used to check if the user exists.</param>
|
|
||||||
/// <param name="token">The token generator.</param>
|
|
||||||
public AuthView(IUserRepository users, ITokenController token)
|
|
||||||
{
|
|
||||||
_users = users;
|
|
||||||
_token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Login.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Login as a user and retrieve an access and a refresh token.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="request">The body of the request.</param>
|
|
||||||
/// <returns>A new access and a refresh token.</returns>
|
|
||||||
/// <response code="400">The user and password does not match.</response>
|
|
||||||
[HttpPost("login")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
||||||
public async Task<ActionResult<JwtToken>> 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." });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Register.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Register a new user and get a new access/refresh token for this new user.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="request">The body of the request.</param>
|
|
||||||
/// <returns>A new access and a refresh token.</returns>
|
|
||||||
/// <response code="400">The request is invalid.</response>
|
|
||||||
/// <response code="409">A user already exists with this username or email address.</response>
|
|
||||||
[HttpPost("register")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
|
||||||
public async Task<ActionResult<JwtToken>> 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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Refresh a token.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// 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.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="token">A valid refresh token.</param>
|
|
||||||
/// <returns>A new access and refresh token.</returns>
|
|
||||||
/// <response code="400">The given refresh token is invalid.</response>
|
|
||||||
[HttpGet("refresh")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
||||||
public async Task<ActionResult<JwtToken>> 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<AniListOptions> anilist)
|
|
||||||
{
|
|
||||||
Dictionary<string, string> 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<ActionResult<User>> 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<ActionResult<JwtToken>> 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<ActionResult<User>> GetMe()
|
|
||||||
{
|
|
||||||
if (!int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out int userID))
|
|
||||||
return BadRequest("Invalid access token");
|
|
||||||
return await _users.GetById(userID);
|
|
||||||
}
|
|
||||||
}
|
|
174
src/Kyoo.Authentication/Views/AuthApi.cs
Normal file
174
src/Kyoo.Authentication/Views/AuthApi.cs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sign in, Sign up or refresh tokens.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/auth")]
|
||||||
|
[ApiDefinition("Authentication", Group = UsersGroup)]
|
||||||
|
public class AuthApi : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The repository to handle users.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IUserRepository _users;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The token generator.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ITokenController _token;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="AuthApi"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="users">The repository used to check if the user exists.</param>
|
||||||
|
/// <param name="token">The token generator.</param>
|
||||||
|
public AuthApi(IUserRepository users, ITokenController token)
|
||||||
|
{
|
||||||
|
_users = users;
|
||||||
|
_token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Login.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Login as a user and retrieve an access and a refresh token.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="request">The body of the request.</param>
|
||||||
|
/// <returns>A new access and a refresh token.</returns>
|
||||||
|
/// <response code="400">The user and password does not match.</response>
|
||||||
|
[HttpPost("login")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
||||||
|
public async Task<ActionResult<JwtToken>> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Register a new user and get a new access/refresh token for this new user.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="request">The body of the request.</param>
|
||||||
|
/// <returns>A new access and a refresh token.</returns>
|
||||||
|
/// <response code="400">The request is invalid.</response>
|
||||||
|
/// <response code="409">A user already exists with this username or email address.</response>
|
||||||
|
[HttpPost("register")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(RequestError))]
|
||||||
|
public async Task<ActionResult<JwtToken>> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refresh a token.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Refresh an access token using the given refresh token. A new access and refresh token are generated.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="token">A valid refresh token.</param>
|
||||||
|
/// <returns>A new access and refresh token.</returns>
|
||||||
|
/// <response code="400">The given refresh token is invalid.</response>
|
||||||
|
[HttpGet("refresh")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
||||||
|
public async Task<ActionResult<JwtToken>> 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<ActionResult<User>> GetMe()
|
||||||
|
{
|
||||||
|
if (!int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out int userID))
|
||||||
|
return BadRequest("Invalid access token");
|
||||||
|
return await _users.GetById(userID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user