// 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.");
}
}