diff --git a/src/Kyoo.Authentication/AuthenticationModule.cs b/src/Kyoo.Authentication/AuthenticationModule.cs
index 604521ca..8b9d7e62 100644
--- a/src/Kyoo.Authentication/AuthenticationModule.cs
+++ b/src/Kyoo.Authentication/AuthenticationModule.cs
@@ -27,13 +27,11 @@ using Kyoo.Abstractions.Controllers;
using Kyoo.Authentication.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.IdentityModel.Tokens;
-using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
namespace Kyoo.Authentication
{
@@ -63,21 +61,13 @@ namespace Kyoo.Authentication
///
private readonly IConfiguration _configuration;
- ///
- /// The environment information to check if the app runs in debug mode
- ///
- private readonly IWebHostEnvironment _environment;
-
///
/// Create a new authentication module instance and use the given configuration and environment.
///
/// The configuration to use
- /// The environment information to check if the app runs in debug mode
- public AuthenticationModule(IConfiguration configuration,
- IWebHostEnvironment environment)
+ public AuthenticationModule(IConfiguration configuration)
{
_configuration = configuration;
- _environment = environment;
}
///
@@ -132,21 +122,8 @@ namespace Kyoo.Authentication
}, SA.StaticFiles),
SA.New(app =>
{
- app.UseCookiePolicy(new CookiePolicyOptions
- {
- MinimumSameSitePolicy = SameSiteMode.Strict
- });
app.UseAuthentication();
}, SA.Authentication),
- SA.New(app =>
- {
- app.Use((ctx, next) =>
- {
- ctx.SetIdentityServerOrigin(_configuration.GetPublicUrl().ToString());
- return next();
- });
- app.UseIdentityServer();
- }, SA.Endpoint),
SA.New(app => app.UseAuthorization(), SA.Authorization)
};
}
diff --git a/src/Kyoo.Authentication/Controllers/ITokenController.cs b/src/Kyoo.Authentication/Controllers/ITokenController.cs
new file mode 100644
index 00000000..1bb29c03
--- /dev/null
+++ b/src/Kyoo.Authentication/Controllers/ITokenController.cs
@@ -0,0 +1,54 @@
+// 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.Threading.Tasks;
+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
+{
+ ///
+ /// 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);
+
+ ///
+ /// 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/Controllers/PasswordUtils.cs b/src/Kyoo.Authentication/Controllers/PasswordUtils.cs
deleted file mode 100644
index 200c5346..00000000
--- a/src/Kyoo.Authentication/Controllers/PasswordUtils.cs
+++ /dev/null
@@ -1,77 +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.Linq;
-using System.Security.Cryptography;
-using IdentityModel;
-
-namespace Kyoo.Authentication
-{
- ///
- /// Some functions to handle password management.
- ///
- public static class PasswordUtils
- {
- ///
- /// Generate an OneTimeAccessCode.
- ///
- /// A new otac.
- public static string GenerateOTAC()
- {
- return CryptoRandom.CreateUniqueId();
- }
-
- ///
- /// Hash a password to store it has a verification only.
- ///
- /// The password to hash
- /// The hashed password
- public static string HashPassword(string password)
- {
- byte[] salt = new byte[16];
-#pragma warning disable SYSLIB0023
- new RNGCryptoServiceProvider().GetBytes(salt);
-#pragma warning restore SYSLIB0023
- Rfc2898DeriveBytes pbkdf2 = new(password, salt, 100000);
- byte[] hash = pbkdf2.GetBytes(20);
- byte[] hashBytes = new byte[36];
- Array.Copy(salt, 0, hashBytes, 0, 16);
- Array.Copy(hash, 0, hashBytes, 16, 20);
- return Convert.ToBase64String(hashBytes);
- }
-
- ///
- /// Check if a password is the same as a valid hashed password.
- ///
- /// The password to check
- ///
- /// The valid hashed password. This password must be hashed via .
- ///
- /// True if the password is valid, false otherwise.
- public static bool CheckPassword(string password, string validPassword)
- {
- byte[] validHash = Convert.FromBase64String(validPassword);
- byte[] salt = new byte[16];
- Array.Copy(validHash, 0, salt, 0, 16);
- Rfc2898DeriveBytes pbkdf2 = new(password, salt, 100000);
- byte[] hash = pbkdf2.GetBytes(20);
- return hash.SequenceEqual(validHash.Skip(16));
- }
- }
-}
diff --git a/src/Kyoo.Authentication/Controllers/TokenController.cs b/src/Kyoo.Authentication/Controllers/TokenController.cs
new file mode 100644
index 00000000..aa305b13
--- /dev/null
+++ b/src/Kyoo.Authentication/Controllers/TokenController.cs
@@ -0,0 +1,149 @@
+// 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.Globalization;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+using Kyoo.Abstractions;
+using Kyoo.Abstractions.Models;
+using Kyoo.Authentication.Models;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Kyoo.Authentication;
+
+///
+/// The service that controls jwt creation and validation.
+///
+public class TokenController : ITokenController
+{
+ ///
+ /// The options that this controller will use.
+ ///
+ private readonly IOptions _options;
+
+ ///
+ /// The configuration used to retrieve the public URL of kyoo.
+ ///
+ private readonly IConfiguration _configuration;
+
+ ///
+ /// Create a new .
+ ///
+ /// The options that this controller will use.
+ /// The configuration used to retrieve the public URL of kyoo.
+ public TokenController(IOptions options, IConfiguration configuration)
+ {
+ _options = options;
+ _configuration = configuration;
+ }
+
+ ///
+ public string CreateAccessToken(User user, out TimeSpan expireIn)
+ {
+ if (user == null)
+ throw new ArgumentNullException(nameof(user));
+
+ expireIn = new TimeSpan(1, 0, 0);
+
+ SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Value.Secret));
+ SigningCredentials credential = new(key, SecurityAlgorithms.HmacSha256Signature);
+ string permissions = user.Permissions != null
+ ? string.Join(',', user.Permissions)
+ : string.Empty;
+ List claims = new()
+ {
+ new Claim(ClaimTypes.NameIdentifier, user.ID.ToString(CultureInfo.InvariantCulture)),
+ new Claim(ClaimTypes.Name, user.Username),
+ new Claim(ClaimTypes.Role, permissions),
+ new Claim("type", "access")
+ };
+ if (user.Email != null)
+ claims.Add(new Claim(ClaimTypes.Email, user.Email));
+ JwtSecurityToken token = new(
+ signingCredentials: credential,
+ issuer: _configuration.GetPublicUrl().ToString(),
+ audience: _configuration.GetPublicUrl().ToString(),
+ claims: claims,
+ expires: DateTime.UtcNow.Add(expireIn)
+ );
+ return new JwtSecurityTokenHandler().WriteToken(token);
+ }
+
+ ///
+ public Task CreateRefreshToken(User user)
+ {
+ if (user == null)
+ throw new ArgumentNullException(nameof(user));
+
+ SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Value.Secret));
+ SigningCredentials credential = new(key, SecurityAlgorithms.HmacSha256Signature);
+ JwtSecurityToken token = new(
+ signingCredentials: credential,
+ issuer: _configuration.GetPublicUrl().ToString(),
+ audience: _configuration.GetPublicUrl().ToString(),
+ claims: new[]
+ {
+ new Claim(ClaimTypes.NameIdentifier, user.ID.ToString(CultureInfo.InvariantCulture)),
+ new Claim("guid", Guid.NewGuid().ToString()),
+ new Claim("type", "refresh")
+ },
+ expires: DateTime.UtcNow.AddYears(1)
+ );
+ // TODO refresh keys are unique (thanks to the guid) but we could store them in DB to invalidate them if requested by the user.
+ return Task.FromResult(new JwtSecurityTokenHandler().WriteToken(token));
+ }
+
+ ///
+ public int GetRefreshTokenUserID(string refreshToken)
+ {
+ SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Value.Secret));
+ JwtSecurityTokenHandler tokenHandler = new();
+ ClaimsPrincipal principal;
+ try
+ {
+ principal = tokenHandler.ValidateToken(refreshToken, new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateIssuerSigningKey = true,
+ ValidateLifetime = true,
+ ValidIssuer = _configuration.GetPublicUrl().ToString(),
+ ValidAudience = _configuration.GetPublicUrl().ToString(),
+ IssuerSigningKey = key
+ }, out SecurityToken _);
+ }
+ catch (Exception ex)
+ {
+ throw new SecurityTokenException(ex.Message);
+ }
+
+ if (principal.Claims.First(x => x.Type == "type").Value != "refresh")
+ throw new SecurityTokenException("Invalid token type. The token should be a refresh token.");
+ Claim identifier = principal.Claims.First(x => x.Type == ClaimTypes.NameIdentifier);
+ if (int.TryParse(identifier.Value, out int id))
+ return id;
+ throw new SecurityTokenException("Token not associated to any user.");
+ }
+}
diff --git a/src/Kyoo.Authentication/Kyoo.Authentication.csproj b/src/Kyoo.Authentication/Kyoo.Authentication.csproj
index fd863584..bf1ebbef 100644
--- a/src/Kyoo.Authentication/Kyoo.Authentication.csproj
+++ b/src/Kyoo.Authentication/Kyoo.Authentication.csproj
@@ -7,6 +7,7 @@
+
diff --git a/src/Kyoo.Authentication/Models/JwtToken.cs b/src/Kyoo.Authentication/Models/JwtToken.cs
new file mode 100644
index 00000000..e0676203
--- /dev/null
+++ b/src/Kyoo.Authentication/Models/JwtToken.cs
@@ -0,0 +1,58 @@
+// 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.Text.Json.Serialization;
+using Newtonsoft.Json;
+
+namespace Kyoo.Authentication;
+
+///
+/// A container representing the response of a login or token refresh.
+///
+public class JwtToken
+{
+ ///
+ /// The type of this token (always a Bearer).
+ ///
+ [JsonProperty("token_token")]
+ [JsonPropertyName("token_type")]
+ public string TokenType => "Bearer";&é"bbbbR"
+
+ ///
+ /// The access token used to authorize requests.
+ ///
+ [JsonProperty("access_token")]
+ [JsonPropertyName("access_token")]
+ public string AccessToken { get; set; }p
+
+ ///
+ /// 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; }
+}
diff --git a/src/Kyoo.Authentication/Models/Options/AuthenticationOption.cs b/src/Kyoo.Authentication/Models/Options/AuthenticationOption.cs
index dc8209aa..92a51e43 100644
--- a/src/Kyoo.Authentication/Models/Options/AuthenticationOption.cs
+++ b/src/Kyoo.Authentication/Models/Options/AuthenticationOption.cs
@@ -16,31 +16,30 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see .
-namespace Kyoo.Authentication.Models
+namespace Kyoo.Authentication.Models;
+
+///
+/// The main authentication options.
+///
+public class AuthenticationOption
{
///
- /// The main authentication options.
+ /// The path to get this option from the root configuration.
///
- public class AuthenticationOption
- {
- ///
- /// The path to get this option from the root configuration.
- ///
- public const string Path = "authentication";
+ public const string Path = "authentication";
- ///
- /// The secret used to encrypt the jwt.
- ///
- public string Secret { get; set; }
+ ///
+ /// The secret used to encrypt the jwt.
+ ///
+ public string Secret { get; set; }
- ///
- /// Options for permissions
- ///
- public PermissionOption Permissions { get; set; }
+ ///
+ /// Options for permissions
+ ///
+ public PermissionOption Permissions { get; set; }
- ///
- /// Root path of user's profile pictures.
- ///
- public string ProfilePicturePath { get; set; }
- }
+ ///
+ /// Root path of user's profile pictures.
+ ///
+ public string ProfilePicturePath { get; set; }
}
diff --git a/src/Kyoo.Authentication/Views/AccountApi.cs b/src/Kyoo.Authentication/Views/AccountApi.cs
index a2cbf2f2..2957c73f 100644
--- a/src/Kyoo.Authentication/Views/AccountApi.cs
+++ b/src/Kyoo.Authentication/Views/AccountApi.cs
@@ -18,246 +18,195 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
-using IdentityServer4.Extensions;
-using IdentityServer4.Models;
-using IdentityServer4.Services;
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;
+using Kyoo.Authentication;
using Kyoo.Authentication.Models.DTO;
-using Microsoft.AspNetCore.Authentication;
+using Kyoo.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
-using static Kyoo.Abstractions.Models.Utils.Constants;
+using Microsoft.IdentityModel.Tokens;
+using BCryptNet = BCrypt.Net.BCrypt;
-namespace Kyoo.Authentication.Views
+namespace Amadeus.Server.Views.Auth;
+
+///
+/// Sign in, Sign up or refresh tokens.
+///
+[ApiController]
+[Route("auth")]
+public class AuthView : ControllerBase
{
///
- /// The endpoint responsible for login, logout, permissions and claims of a user.
- /// Documentation of this endpoint is a work in progress.
+ /// The repository used to check if the user exists.
///
- /// TODO document this well.
- [Route("api/accounts")]
- [Route("api/account", Order = AlternativeRoute)]
- [ApiController]
- [ApiDefinition("Account")]
- public class AccountApi : Controller, IProfileService
+ 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)
{
- ///
- /// The repository to handle users.
- ///
- private readonly IUserRepository _users;
+ _users = users;
+ _token = token;
+ }
- ///
- /// A file manager to send profile pictures
- ///
- private readonly IFileSystem _files;
-
- ///
- /// Options about authentication. Those options are monitored and reloads are supported.
- ///
- private readonly IOptions _options;
-
- ///
- /// Create a new handle to handle login/users requests.
- ///
- /// The user repository to create and manage users
- /// A file manager to send profile pictures
- /// Authentication options (this may be hot reloaded)
- public AccountApi(IUserRepository users,
- IFileSystem files,
- IOptions options)
+ ///
+ /// 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))
{
- _users = users;
- _files = files;
- _options = options;
- }
-
- ///
- /// Register
- ///
- ///
- /// Register a new user and return a OTAC to connect to it.
- ///
- /// The DTO register request
- /// A OTAC to connect to this new account
- [HttpPost("register")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(RequestError))]
- public async Task> Register([FromBody] RegisterRequest request)
- {
- User user = request.ToUser();
- user.Permissions = _options.Value.Permissions.NewUser;
- user.Password = PasswordUtils.HashPassword(user.Password);
- user.ExtraData["otac"] = PasswordUtils.GenerateOTAC();
- user.ExtraData["otac-expire"] = DateTime.Now.AddMinutes(1).ToString("s");
- try
+ return new JwtToken
{
- await _users.Create(user);
- }
- catch (DuplicatedItemException)
- {
- return Conflict(new RequestError("A user with this name already exists"));
- }
-
- return Ok(new OtacResponse(user.ExtraData["otac"]));
- }
-
- ///
- /// Return an authentication properties based on a stay login property
- ///
- /// Should the user stay logged
- /// Authentication properties based on a stay login
- private static AuthenticationProperties _StayLogged(bool stayLogged)
- {
- if (!stayLogged)
- return null;
- return new AuthenticationProperties
- {
- IsPersistent = true,
- ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(1)
+ 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." });
+ }
- ///
- /// Login
- ///
- ///
- /// Login the current session.
- ///
- /// The DTO login request
- /// TODO
- [HttpPost("login")]
- public async Task Login([FromBody] LoginRequest login)
+ ///
+ /// 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
{
- User user = await _users.GetOrDefault(x => x.Username == login.Username);
-
- if (user == null)
- return Unauthorized();
- if (!PasswordUtils.CheckPassword(login.Password, user.Password))
- return Unauthorized();
-
- await HttpContext.SignInAsync(user.ToIdentityUser(), _StayLogged(login.StayLoggedIn));
- return Ok(new { RedirectUrl = login.ReturnURL, IsOk = true });
+ await _users.Create(user);
+ }
+ catch (DuplicateField)
+ {
+ return Conflict(new { Message = "A user already exists with this username." });
}
- ///
- /// Use a OTAC to login a user.
- ///
- /// The OTAC request
- /// TODO
- [HttpPost("otac-login")]
- public async Task OtacLogin([FromBody] OtacRequest otac)
+
+ return new JwtToken
{
- // TODO once hstore (Dictionary accessor) are supported, use them.
- // We retrieve all users, this is inefficient.
- User user = (await _users.GetAll()).FirstOrDefault(x => x.ExtraData.GetValueOrDefault("otac") == otac.Otac);
- if (user == null)
- return Unauthorized();
- if (DateTime.ParseExact(user.ExtraData["otac-expire"], "s", CultureInfo.InvariantCulture) <=
- DateTime.UtcNow)
+ 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
{
- return BadRequest(new
- {
- code = "ExpiredOTAC",
- description = "The OTAC has expired. Try to login with your password."
- });
- }
-
- await HttpContext.SignInAsync(user.ToIdentityUser(), _StayLogged(otac.StayLoggedIn));
- return Ok();
+ AccessToken = _token.CreateAccessToken(user, out TimeSpan expireDate),
+ RefreshToken = await _token.CreateRefreshToken(user),
+ ExpireIn = expireDate
+ };
}
-
- ///
- /// Sign out an user
- ///
- /// TODO
- [HttpGet("logout")]
- [Authorize]
- public async Task Logout()
+ catch (ElementNotFound)
{
- await HttpContext.SignOutAsync();
- return Ok();
+ return BadRequest(new { Message = "Invalid refresh token." });
}
-
- ///
- [ApiExplorerSettings(IgnoreApi = true)]
- public async Task GetProfileDataAsync(ProfileDataRequestContext context)
+ catch (SecurityTokenException ex)
{
- User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId()));
- if (user == null)
- return;
- context.IssuedClaims.AddRange(user.GetClaims());
- context.IssuedClaims.Add(new Claim("permissions", string.Join(',', user.Permissions)));
- }
-
- ///
- [ApiExplorerSettings(IgnoreApi = true)]
- public async Task IsActiveAsync(IsActiveContext context)
- {
- User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId()));
- context.IsActive = user != null;
- }
-
- ///
- /// Get the user's profile picture.
- ///
- /// The user slug
- /// The profile picture of the user or 404 if not found
- [HttpGet("picture/{slug}")]
- public async Task GetPicture(string slug)
- {
- User user = await _users.GetOrDefault(slug);
- if (user == null)
- return NotFound();
- string path = Path.Combine(_options.Value.ProfilePicturePath, user.ID.ToString());
- return _files.FileResult(path);
- }
-
- ///
- /// Update profile information (email, username, profile picture...)
- ///
- /// The new information
- /// The edited user
- [HttpPut]
- [Authorize]
- public async Task> Update([FromForm] AccountUpdateRequest data)
- {
- User user = await _users.GetOrDefault(int.Parse(HttpContext.User.GetSubjectId()));
-
- if (user == null)
- return Unauthorized();
- if (!string.IsNullOrEmpty(data.Email))
- user.Email = data.Email;
- if (!string.IsNullOrEmpty(data.Username))
- user.Username = data.Username;
- if (data.Picture?.Length > 0)
- {
- string path = _files.Combine(_options.Value.ProfilePicturePath, user.ID.ToString());
- await using Stream file = await _files.NewFile(path);
- await data.Picture.CopyToAsync(file);
- }
- return await _users.Edit(user, false);
- }
-
- ///
- /// Get permissions for a non connected user.
- ///
- /// The list of permissions of a default user.
- [HttpGet("permissions")]
- public ActionResult> GetDefaultPermissions()
- {
- return _options.Value.Permissions.Default ?? Array.Empty();
+ 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);
+ }
}