Removing Identity Server and creating a custom jwt

This commit is contained in:
Zoe Roux 2021-12-09 09:20:00 +01:00
parent 425c80237b
commit 3962c0eb7a
No known key found for this signature in database
GPG Key ID: 54F19BB73170955D
8 changed files with 23 additions and 415 deletions

View File

@ -18,27 +18,21 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Autofac;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using Kyoo.Abstractions;
using Kyoo.Abstractions.Controllers;
using Kyoo.Authentication.Models;
using Kyoo.Authentication.Views;
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.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Tokens;
using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
namespace Kyoo.Authentication
@ -55,14 +49,13 @@ namespace Kyoo.Authentication
public string Name => "Authentication";
/// <inheritdoc />
public string Description => "Enable OpenID authentication for Kyoo.";
public string Description => "Enable an authentication/permission system for Kyoo (via Jwt or ApKeys).";
/// <inheritdoc />
public Dictionary<string, Type> Configuration => new()
{
{ AuthenticationOption.Path, typeof(AuthenticationOption) },
{ PermissionOption.Path, typeof(PermissionOption) },
{ CertificateOption.Path, typeof(CertificateOption) }
};
/// <summary>
@ -70,11 +63,6 @@ namespace Kyoo.Authentication
/// </summary>
private readonly IConfiguration _configuration;
/// <summary>
/// The logger used to allow IdentityServer to log things.
/// </summary>
private readonly ILogger<DefaultCorsPolicyService> _logger;
/// <summary>
/// The environment information to check if the app runs in debug mode
/// </summary>
@ -84,16 +72,11 @@ namespace Kyoo.Authentication
/// Create a new authentication module instance and use the given configuration and environment.
/// </summary>
/// <param name="configuration">The configuration to use</param>
/// <param name="logger">The logger used to allow IdentityServer to log things</param>
/// <param name="environment">The environment information to check if the app runs in debug mode</param>
[SuppressMessage("ReSharper", "ContextualLoggerProblem",
Justification = "The logger is used for a dependency that is not created via the container.")]
public AuthenticationModule(IConfiguration configuration,
ILogger<DefaultCorsPolicyService> logger,
IWebHostEnvironment environment)
{
_configuration = configuration;
_logger = logger;
_environment = environment;
}
@ -101,59 +84,29 @@ namespace Kyoo.Authentication
public void Configure(ContainerBuilder builder)
{
builder.RegisterType<PermissionValidator>().As<IPermissionValidator>().SingleInstance();
DefaultCorsPolicyService cors = new(_logger)
{
AllowedOrigins = { _configuration.GetPublicUrl().GetLeftPart(UriPartial.Authority) }
};
builder.RegisterInstance(cors).As<ICorsPolicyService>().SingleInstance();
}
/// <inheritdoc />
public void Configure(IServiceCollection services)
{
Uri publicUrl = _configuration.GetPublicUrl();
if (_environment.IsDevelopment())
IdentityModelEventSource.ShowPII = true;
services.AddControllers();
AuthenticationOption jwt = new();
_configuration.GetSection(AuthenticationOption.Path).Bind(jwt);
// TODO handle direct-videos with bearers (probably add a cookie and a app.Use to translate that for videos)
// TODO Check if tokens should be stored.
List<Client> clients = new();
_configuration.GetSection("authentication:clients").Bind(clients);
CertificateOption certificateOptions = new();
_configuration.GetSection(CertificateOption.Path).Bind(certificateOptions);
clients.AddRange(IdentityContext.GetClients());
foreach (Client client in clients)
{
client.RedirectUris = client.RedirectUris
.Select(x => x.StartsWith("/") ? publicUrl.ToString().TrimEnd('/') + x : x)
.ToArray();
}
services.AddIdentityServer(options =>
{
options.IssuerUri = publicUrl.ToString();
options.UserInteraction.LoginUrl = $"{publicUrl}login";
options.UserInteraction.ErrorUrl = $"{publicUrl}error";
options.UserInteraction.LogoutUrl = $"{publicUrl}logout";
})
.AddInMemoryIdentityResources(IdentityContext.GetIdentityResources())
.AddInMemoryApiScopes(IdentityContext.GetScopes())
.AddInMemoryApiResources(IdentityContext.GetApis())
.AddInMemoryClients(clients)
.AddProfileService<AccountApi>()
.AddSigninKeys(certificateOptions);
services.AddAuthentication()
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = publicUrl.ToString();
options.Audience = "kyoo";
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = publicUrl.ToString(),
ValidAudience = publicUrl.ToString(),
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwt.Secret))
};
});
}

View File

@ -1,137 +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.IO;
using System.Security.Cryptography.X509Certificates;
using Kyoo.Authentication.Models;
using Microsoft.Extensions.DependencyInjection;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Operators;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities;
using Org.BouncyCastle.X509;
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;
namespace Kyoo.Authentication
{
/// <summary>
/// A class containing multiple extensions methods to manage certificates.
/// </summary>
public static class Certificates
{
/// <summary>
/// Add the certificate file to the identity server. If the certificate will expire soon, automatically renew it.
/// If no certificate exists, one is generated.
/// </summary>
/// <param name="builder">The identity server that will be modified.</param>
/// <param name="options">The certificate options</param>
/// <returns>The initial builder to allow chain-calls.</returns>
public static IIdentityServerBuilder AddSigninKeys(this IIdentityServerBuilder builder,
CertificateOption options)
{
X509Certificate2 certificate = _GetCertificate(options);
builder.AddSigningCredential(certificate);
if (certificate.NotAfter.AddDays(-7) <= DateTime.UtcNow)
{
Console.WriteLine("Signin certificate will expire soon, renewing it.");
if (File.Exists(options.OldFile))
File.Delete(options.OldFile);
File.Move(options.File, options.OldFile);
builder.AddValidationKey(_GenerateCertificate(options.File, options.Password));
}
else if (File.Exists(options.OldFile))
builder.AddValidationKey(_GetExistingCredential(options.OldFile, options.Password));
return builder;
}
/// <summary>
/// Get or generate the sign-in certificate.
/// </summary>
/// <param name="options">The certificate options</param>
/// <returns>A valid certificate</returns>
private static X509Certificate2 _GetCertificate(CertificateOption options)
{
return File.Exists(options.File)
? _GetExistingCredential(options.File, options.Password)
: _GenerateCertificate(options.File, options.Password);
}
/// <summary>
/// Load a certificate from a file
/// </summary>
/// <param name="file">The path of the certificate</param>
/// <param name="password">The password of the certificate</param>
/// <returns>The loaded certificate</returns>
private static X509Certificate2 _GetExistingCredential(string file, string password)
{
X509KeyStorageFlags storeFlags = X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.PersistKeySet |
X509KeyStorageFlags.Exportable;
return new X509Certificate2(file, password, storeFlags);
}
/// <summary>
/// Generate a new certificate key and put it in the file at <paramref name="file"/>.
/// </summary>
/// <param name="file">The path of the output file</param>
/// <param name="password">The password of the new certificate</param>
/// <returns>The generated certificate</returns>
private static X509Certificate2 _GenerateCertificate(string file, string password)
{
SecureRandom random = new();
X509V3CertificateGenerator certificateGenerator = new();
certificateGenerator.SetSerialNumber(BigIntegers.CreateRandomInRange(BigInteger.One,
BigInteger.ValueOf(long.MaxValue), random));
certificateGenerator.SetIssuerDN(new X509Name($"C=NL, O=SDG, CN=Kyoo"));
certificateGenerator.SetSubjectDN(new X509Name($"C=NL, O=SDG, CN=Kyoo"));
certificateGenerator.SetNotBefore(DateTime.UtcNow.Date);
certificateGenerator.SetNotAfter(DateTime.UtcNow.Date.AddMonths(3));
KeyGenerationParameters keyGenerationParameters = new(random, 2048);
RsaKeyPairGenerator keyPairGenerator = new();
keyPairGenerator.Init(keyGenerationParameters);
AsymmetricCipherKeyPair subjectKeyPair = keyPairGenerator.GenerateKeyPair();
certificateGenerator.SetPublicKey(subjectKeyPair.Public);
const string signatureAlgorithm = "MD5WithRSA";
Asn1SignatureFactory signatureFactory = new(signatureAlgorithm, subjectKeyPair.Private);
X509Certificate bouncyCert = certificateGenerator.Generate(signatureFactory);
Pkcs12Store store = new Pkcs12StoreBuilder().Build();
store.SetKeyEntry("Kyoo_key", new AsymmetricKeyEntry(subjectKeyPair.Private), new[]
{
new X509CertificateEntry(bouncyCert)
});
using MemoryStream pfxStream = new();
store.Save(pfxStream, password.ToCharArray(), random);
X509Certificate2 certificate = new(pfxStream.ToArray(), password, X509KeyStorageFlags.Exportable);
using FileStream fileStream = File.OpenWrite(file);
pfxStream.WriteTo(fileStream);
return certificate;
}
}
}

View File

@ -19,9 +19,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using IdentityModel;
using IdentityServer4;
using Kyoo.Abstractions.Models;
namespace Kyoo.Authentication
{
@ -30,35 +27,6 @@ namespace Kyoo.Authentication
/// </summary>
public static class Extensions
{
/// <summary>
/// Get claims of an user.
/// </summary>
/// <param name="user">The user concerned</param>
/// <returns>The list of claims the user has</returns>
public static ICollection<Claim> GetClaims(this User user)
{
return new[]
{
new Claim(JwtClaimTypes.Subject, user.ID.ToString()),
new Claim(JwtClaimTypes.Name, user.Username),
new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}")
};
}
/// <summary>
/// Convert a user to an IdentityServerUser.
/// </summary>
/// <param name="user">The user to convert.</param>
/// <returns>The corresponding identity server user.</returns>
public static IdentityServerUser ToIdentityUser(this User user)
{
return new IdentityServerUser(user.ID.ToString())
{
DisplayName = user.Username,
AdditionalClaims = new[] { new Claim("permissions", string.Join(',', user.Permissions)) }
};
}
/// <summary>
/// Get the permissions of an user.
/// </summary>

View File

@ -5,11 +5,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="4.1.2" />
<PackageReference Include="IdentityServer4.Storage" Version="4.1.2" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.10" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.12" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.2" />
<ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" />
</ItemGroup>

View File

@ -1,121 +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.Collections.Generic;
using System.Linq;
using IdentityServer4.Models;
namespace Kyoo.Authentication
{
/// <summary>
/// The hard coded context of the identity server.
/// </summary>
public static class IdentityContext
{
/// <summary>
/// The list of identity resources supported (email, profile and openid)
/// </summary>
/// <returns>The list of identity resources supported</returns>
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Email(),
new IdentityResources.Profile()
};
}
/// <summary>
/// Get the list of officially supported clients.
/// </summary>
/// <remarks>
/// You can add custom clients in the settings.json file.
/// </remarks>
/// <returns>The list of officially supported clients.</returns>
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new()
{
ClientId = "kyoo.webapp",
AccessTokenType = AccessTokenType.Jwt,
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
AllowAccessTokensViaBrowser = true,
AllowOfflineAccess = true,
RequireConsent = false,
AllowedScopes = { "openid", "profile", "kyoo.read", "kyoo.write", "kyoo.play", "kyoo.admin" },
RedirectUris = { "/", "/silent.html" },
PostLogoutRedirectUris = { "/logout" }
}
};
}
/// <summary>
/// The list of scopes supported by the API.
/// </summary>
/// <returns>The list of scopes</returns>
public static IEnumerable<ApiScope> GetScopes()
{
return new[]
{
new ApiScope
{
Name = "kyoo.read",
DisplayName = "Read only access to the API.",
},
new ApiScope
{
Name = "kyoo.write",
DisplayName = "Read and write access to the public API"
},
new ApiScope
{
Name = "kyoo.play",
DisplayName = "Allow playback of movies and episodes."
},
new ApiScope
{
Name = "kyoo.admin",
DisplayName = "Full access to the admin's API and the public API."
}
};
}
/// <summary>
/// The list of APIs (this is used to create Audiences)
/// </summary>
/// <returns>The list of apis</returns>
public static IEnumerable<ApiResource> GetApis()
{
return new[]
{
new ApiResource("kyoo", "Kyoo")
{
Scopes = GetScopes().Select(x => x.Name).ToArray()
}
};
}
}
}

View File

@ -29,9 +29,9 @@ namespace Kyoo.Authentication.Models
public const string Path = "authentication";
/// <summary>
/// The options for certificates
/// The secret used to encrypt the jwt.
/// </summary>
public CertificateOption Certificate { get; set; }
public string Secret { get; set; }
/// <summary>
/// Options for permissions

View File

@ -1,46 +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/>.
namespace Kyoo.Authentication.Models
{
/// <summary>
/// A typed option model for the certificate
/// </summary>
public class CertificateOption
{
/// <summary>
/// The path to get this option from the root configuration.
/// </summary>
public const string Path = "authentication:certificate";
/// <summary>
/// The path of the certificate file.
/// </summary>
public string File { get; set; }
/// <summary>
/// The path of the old certificate file.
/// </summary>
public string OldFile { get; set; }
/// <summary>
/// The password of the certificates.
/// </summary>
public string Password { get; set; }
}
}

View File

@ -60,17 +60,11 @@
},
"authentication": {
"certificate": {
"file": "certificate.pfx",
"oldFile": "oldCertificate.pfx",
"password": "passphrase"
},
"permissions": {
"default": ["overall.read", "overall.write", "overall.create", "overall.delete", "admin.read", "admin.write"],
"newUser": ["overall.read", "overall.write", "overall.create", "overall.delete", "admin.read", "admin.write"]
},
"profilePicturePath": "users/",
"clients": []
},
"tvdb": {