jwt: Starting to rework authentication

This commit is contained in:
Zoe Roux 2022-04-17 15:03:26 +02:00
parent 3962c0eb7a
commit 1a534d2325
No known key found for this signature in database
GPG Key ID: 54F19BB73170955D
8 changed files with 442 additions and 332 deletions

View File

@ -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
/// </summary>
private readonly IConfiguration _configuration;
/// <summary>
/// The environment information to check if the app runs in debug mode
/// </summary>
private readonly IWebHostEnvironment _environment;
/// <summary>
/// Create a new authentication module instance and use the given configuration and environment.
/// </summary>
/// <param name="configuration">The configuration to use</param>
/// <param name="environment">The environment information to check if the app runs in debug mode</param>
public AuthenticationModule(IConfiguration configuration,
IWebHostEnvironment environment)
public AuthenticationModule(IConfiguration configuration)
{
_configuration = configuration;
_environment = environment;
}
/// <inheritdoc />
@ -132,21 +122,8 @@ namespace Kyoo.Authentication
}, SA.StaticFiles),
SA.New<IApplicationBuilder>(app =>
{
app.UseCookiePolicy(new CookiePolicyOptions
{
MinimumSameSitePolicy = SameSiteMode.Strict
});
app.UseAuthentication();
}, SA.Authentication),
SA.New<IApplicationBuilder>(app =>
{
app.Use((ctx, next) =>
{
ctx.SetIdentityServerOrigin(_configuration.GetPublicUrl().ToString());
return next();
});
app.UseIdentityServer();
}, SA.Endpoint),
SA.New<IApplicationBuilder>(app => app.UseAuthorization(), SA.Authorization)
};
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models;
using Microsoft.IdentityModel.Tokens;
namespace Kyoo.Authentication;
/// <summary>
/// The service that controls jwt creation and validation.
/// </summary>
public interface ITokenController
{
/// <summary>
/// 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>
/// Create a new refresh token for the given user.
/// </summary>
/// <param name="user">The user to create a token for.</param>
/// <returns>A new, valid refresh token.</returns>
Task<string> CreateRefreshToken([NotNull] User user);
/// <summary>
/// Check if the given refresh token is valid and if it is, retrieve the id of the user this token belongs to.
/// </summary>
/// <param name="refreshToken">The refresh token to validate.</param>
/// <exception cref="SecurityTokenException">The given refresh token is not valid.</exception>
/// <returns>The id of the token's user.</returns>
int GetRefreshTokenUserID(string refreshToken);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Linq;
using System.Security.Cryptography;
using IdentityModel;
namespace Kyoo.Authentication
{
/// <summary>
/// Some functions to handle password management.
/// </summary>
public static class PasswordUtils
{
/// <summary>
/// Generate an OneTimeAccessCode.
/// </summary>
/// <returns>A new otac.</returns>
public static string GenerateOTAC()
{
return CryptoRandom.CreateUniqueId();
}
/// <summary>
/// Hash a password to store it has a verification only.
/// </summary>
/// <param name="password">The password to hash</param>
/// <returns>The hashed password</returns>
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);
}
/// <summary>
/// Check if a password is the same as a valid hashed password.
/// </summary>
/// <param name="password">The password to check</param>
/// <param name="validPassword">
/// The valid hashed password. This password must be hashed via <see cref="HashPassword"/>.
/// </param>
/// <returns>True if the password is valid, false otherwise.</returns>
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));
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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;
/// <summary>
/// The service that controls jwt creation and validation.
/// </summary>
public class TokenController : ITokenController
{
/// <summary>
/// The options that this controller will use.
/// </summary>
private readonly IOptions<AuthenticationOption> _options;
/// <summary>
/// The configuration used to retrieve the public URL of kyoo.
/// </summary>
private readonly IConfiguration _configuration;
/// <summary>
/// Create a new <see cref="TokenController"/>.
/// </summary>
/// <param name="options">The options that this controller will use.</param>
/// <param name="configuration">The configuration used to retrieve the public URL of kyoo.</param>
public TokenController(IOptions<AuthenticationOption> options, IConfiguration configuration)
{
_options = options;
_configuration = configuration;
}
/// <inheritdoc />
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<Claim> 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);
}
/// <inheritdoc />
public Task<string> 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));
}
/// <inheritdoc />
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.");
}
}

View File

@ -7,6 +7,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.12" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" />
</ItemGroup>

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace Kyoo.Authentication;
/// <summary>
/// A container representing the response of a login or token refresh.
/// </summary>
public class JwtToken
{
/// <summary>
/// The type of this token (always a Bearer).
/// </summary>
[JsonProperty("token_token")]
[JsonPropertyName("token_type")]
public string TokenType => "Bearer";&é"bbbbR"
/// <summary>
/// The access token used to authorize requests.
/// </summary>
[JsonProperty("access_token")]
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }p
/// <summary>
/// The refresh token used to retrieve a new access/refresh token when the access token has expired.
/// </summary>
[JsonProperty("refresh_token")]
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
/// <summary>
/// The date when the access token will expire. After this date, the refresh token should be used to retrieve.
/// a new token.cs
/// </summary>
[JsonProperty("expire_in")]
[JsonPropertyName("expire_in")]
public TimeSpan ExpireIn { get; set; }
}

View File

@ -16,8 +16,8 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
namespace Kyoo.Authentication.Models
{
namespace Kyoo.Authentication.Models;
/// <summary>
/// The main authentication options.
/// </summary>
@ -43,4 +43,3 @@ namespace Kyoo.Authentication.Models
/// </summary>
public string ProfilePicturePath { get; set; }
}
}

View File

@ -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 Amadeus.Server.Views.Auth;
namespace Kyoo.Authentication.Views
{
/// <summary>
/// The endpoint responsible for login, logout, permissions and claims of a user.
/// Documentation of this endpoint is a work in progress.
/// Sign in, Sign up or refresh tokens.
/// </summary>
/// TODO document this well.
[Route("api/accounts")]
[Route("api/account", Order = AlternativeRoute)]
[ApiController]
[ApiDefinition("Account")]
public class AccountApi : Controller, IProfileService
[Route("auth")]
public class AuthView : ControllerBase
{
/// <summary>
/// The repository to handle users.
/// The repository used to check if the user exists.
/// </summary>
private readonly IUserRepository _users;
/// <summary>
/// A file manager to send profile pictures
/// The token generator.
/// </summary>
private readonly IFileSystem _files;
private readonly ITokenController _token;
/// <summary>
/// Options about authentication. Those options are monitored and reloads are supported.
/// Create a new <see cref="AuthView"/>.
/// </summary>
private readonly IOptions<AuthenticationOption> _options;
/// <summary>
/// Create a new <see cref="AccountApi"/> handle to handle login/users requests.
/// </summary>
/// <param name="users">The user repository to create and manage users</param>
/// <param name="files">A file manager to send profile pictures</param>
/// <param name="options">Authentication options (this may be hot reloaded)</param>
public AccountApi(IUserRepository users,
IFileSystem files,
IOptions<AuthenticationOption> options)
/// <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;
_files = files;
_options = options;
_token = token;
}
/// <summary>
/// Register
/// Login.
/// </summary>
/// <remarks>
/// Register a new user and return a OTAC to connect to it.
/// Login as a user and retrieve an access and a refresh token.
/// </remarks>
/// <param name="request">The DTO register request</param>
/// <returns>A OTAC to connect to this new account</returns>
/// <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.Status409Conflict, Type = typeof(RequestError))]
public async Task<ActionResult<OtacResponse>> Register([FromBody] RegisterRequest request)
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<JwtToken>> 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");
user.Password = BCryptNet.HashPassword(request.Password);
try
{
await _users.Create(user);
}
catch (DuplicatedItemException)
catch (DuplicateField)
{
return Conflict(new RequestError("A user with this name already exists"));
return Conflict(new { Message = "A user already exists with this username." });
}
return Ok(new OtacResponse(user.ExtraData["otac"]));
}
/// <summary>
/// Return an authentication properties based on a stay login property
/// </summary>
/// <param name="stayLogged">Should the user stay logged</param>
/// <returns>Authentication properties based on a stay login</returns>
private static AuthenticationProperties _StayLogged(bool stayLogged)
return new JwtToken
{
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
};
}
/// <summary>
/// Login
/// Refresh a token.
/// </summary>
/// <remarks>
/// Login the current session.
/// 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="login">The DTO login request</param>
/// <returns>TODO</returns>
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest login)
/// <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)
{
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 });
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 });
}
}
/// <summary>
/// Use a OTAC to login a user.
/// </summary>
/// <param name="otac">The OTAC request</param>
/// <returns>TODO</returns>
[HttpPost("otac-login")]
public async Task<IActionResult> OtacLogin([FromBody] OtacRequest otac)
[HttpGet("anilist")]
[ProducesResponseType(StatusCodes.Status302Found)]
public IActionResult AniListLogin([FromQuery] Uri redirectUrl, [FromServices] IOptions<AniListOptions> anilist)
{
// TODO once hstore (Dictionary<string, string> 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)
Dictionary<string, string> query = new()
{
return BadRequest(new
{
code = "ExpiredOTAC",
description = "The OTAC has expired. Try to login with your password."
});
["client_id"] = anilist.Value.ClientID,
["redirect_uri"] = redirectUrl.ToString(),
["response_type"] = "code"
};
return Redirect($"https://anilist.co/api/v2/oauth/authorize{query.ToQueryString()}");
}
await HttpContext.SignInAsync(user.ToIdentityUser(), _StayLogged(otac.StayLoggedIn));
return Ok();
}
/// <summary>
/// Sign out an user
/// </summary>
/// <returns>TODO</returns>
[HttpGet("logout")]
[HttpPost("link/anilist")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize]
public async Task<IActionResult> Logout()
public async Task<ActionResult<User>> AniListLink([FromQuery] string code, [FromServices] AniListService anilist)
{
await HttpContext.SignOutAsync();
return Ok();
// 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);
}
/// <inheritdoc />
[ApiExplorerSettings(IgnoreApi = true)]
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
[HttpPost("login/anilist")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<JwtToken>> AniListLogin([FromQuery] string code, [FromServices] AniListService anilist)
{
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)));
User user = await anilist.Login(code);
return new JwtToken
{
AccessToken = _token.CreateAccessToken(user, out TimeSpan expireIn),
RefreshToken = await _token.CreateRefreshToken(user),
ExpireIn = expireIn
};
}
/// <inheritdoc />
[ApiExplorerSettings(IgnoreApi = true)]
public async Task IsActiveAsync(IsActiveContext context)
{
User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId()));
context.IsActive = user != null;
}
/// <summary>
/// Get the user's profile picture.
/// </summary>
/// <param name="slug">The user slug</param>
/// <returns>The profile picture of the user or 404 if not found</returns>
[HttpGet("picture/{slug}")]
public async Task<IActionResult> 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);
}
/// <summary>
/// Update profile information (email, username, profile picture...)
/// </summary>
/// <param name="data">The new information</param>
/// <returns>The edited user</returns>
[HttpPut]
[HttpGet("me")]
[Authorize]
public async Task<ActionResult<User>> Update([FromForm] AccountUpdateRequest data)
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<User>> GetMe()
{
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);
}
/// <summary>
/// Get permissions for a non connected user.
/// </summary>
/// <returns>The list of permissions of a default user.</returns>
[HttpGet("permissions")]
public ActionResult<IEnumerable<string>> GetDefaultPermissions()
{
return _options.Value.Permissions.Default ?? Array.Empty<string>();
}
if (!int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out int userID))
return BadRequest("Invalid access token");
return await _users.GetById(userID);
}
}