mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Merge pull request #111 from AnonymusRaccoon/jwt
This commit is contained in:
commit
7a8964c336
@ -70,7 +70,6 @@ csharp_indent_case_contents = true
|
||||
csharp_indent_switch_labels = true
|
||||
# Modifiers
|
||||
dotnet_style_readonly_field = true:suggestion
|
||||
csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
|
||||
# Naming style
|
||||
dotnet_naming_symbols.privates.applicable_kinds = property,method,event,delegate
|
||||
|
@ -1,2 +1,3 @@
|
||||
TVDB__APIKEY=
|
||||
THEMOVIEDB__APIKEY=
|
||||
AUTHENTICATION_SECRET=
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -356,3 +356,4 @@ healthchecksdb
|
||||
/Kyoo/TheTVDB-Credentials.json
|
||||
|
||||
.vscode
|
||||
.netcoredbg_hist
|
||||
|
@ -10,7 +10,7 @@ title: Build
|
||||
To develop for Kyoo, you will need the **.NET 5.0 SDK**, **node** and **npm** for the webapp. If you want to build the transcoder, you will also need a cmake compatible environment.
|
||||
|
||||
## Building
|
||||
To run the development server, simply open the .sln file with your favorite C# IDE (like Jetbrain's Rider or Visual Studio) and press run or you can use the CLI and use the ```dotnet run dotnet run -p src/Kyoo.Host.Console --launch-profile "Console"``` command.
|
||||
To run the development server, simply open the .sln file with your favorite C# IDE (like Jetbrain's Rider or Visual Studio) and press run or you can use the CLI and use the ```dotnet run --project src/Kyoo.Host.Console --launch-profile "Console"``` command.
|
||||
To pack the application, run the ```dotnet publish -c Release -o <build_path> Kyoo.Host.Console``` command. This will build the server, the webapp and the transcoder and output files in the <build_path> directory.
|
||||
|
||||
## Skipping parts
|
||||
|
@ -26,7 +26,6 @@
|
||||
"@angular/platform-browser": "^13.0.2",
|
||||
"@angular/platform-browser-dynamic": "^13.0.2",
|
||||
"@angular/router": "^13.0.2",
|
||||
"angular-auth-oidc-client": "^13.0.0",
|
||||
"bootstrap": "^4.6.0",
|
||||
"detect-browser": "^5.2.1",
|
||||
"hls.js": "^1.1.1",
|
||||
|
@ -16,7 +16,6 @@ import { MatSliderModule } from "@angular/material/slider";
|
||||
import { MatTabsModule } from "@angular/material/tabs";
|
||||
import { MatTooltipModule } from "@angular/material/tooltip";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { AuthModule as OidcModule, LogLevel } from "angular-auth-oidc-client";
|
||||
import { tap } from "rxjs/operators";
|
||||
import { AccountComponent } from "./account/account.component";
|
||||
import { LogoutComponent } from "./logout/logout.component";
|
||||
@ -48,25 +47,6 @@ import { UnauthorizedComponent } from "./unauthorized/unauthorized.component";
|
||||
FormsModule,
|
||||
MatTabsModule,
|
||||
MatCheckboxModule,
|
||||
OidcModule.forRoot({
|
||||
config: {
|
||||
authority: window.location.origin,
|
||||
redirectUrl: `${window.location.origin}/`,
|
||||
postLogoutRedirectUri: `${window.location.origin}/logout`,
|
||||
clientId: "kyoo.webapp",
|
||||
responseType: "code",
|
||||
triggerAuthorizationResultEvent: false,
|
||||
scope: "openid profile offline_access kyoo.read kyoo.write kyoo.play kyoo.admin",
|
||||
silentRenew: true,
|
||||
silentRenewUrl: `${window.location.origin}/silent.html`,
|
||||
useRefreshToken: true,
|
||||
startCheckSession: true,
|
||||
|
||||
forbiddenRoute: `${window.location.origin}/forbidden`,
|
||||
unauthorizedRoute: `${window.location.origin}/unauthorized`,
|
||||
logLevel: LogLevel.Warn
|
||||
}
|
||||
}),
|
||||
RouterModule
|
||||
],
|
||||
entryComponents: [
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { LoginResponse, OidcSecurityService } from "angular-auth-oidc-client";
|
||||
import { Account } from "../models/account";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root"
|
||||
@ -11,37 +9,15 @@ export class AuthService
|
||||
isAuthenticated: boolean = false;
|
||||
account: Account = null;
|
||||
|
||||
constructor(private oidcSecurityService: OidcSecurityService, private http: HttpClient)
|
||||
constructor()
|
||||
{
|
||||
this.oidcSecurityService.checkAuth()
|
||||
.subscribe((auth: LoginResponse) => this.isAuthenticated = auth.isAuthenticated);
|
||||
this.oidcSecurityService.userData$.subscribe(x =>
|
||||
{
|
||||
if (x?.userData == null)
|
||||
{
|
||||
this.account = null;
|
||||
this.isAuthenticated = false;
|
||||
return;
|
||||
}
|
||||
this.account = {
|
||||
email: x.userData.email,
|
||||
username: x.userData.username,
|
||||
picture: x.userData.picture,
|
||||
permissions: x.userData.permissions?.split(",") ?? []
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
login(): void
|
||||
{
|
||||
this.oidcSecurityService.authorize();
|
||||
}
|
||||
|
||||
logout(): void
|
||||
{
|
||||
this.http.get("api/account/logout").subscribe(() =>
|
||||
{
|
||||
this.oidcSecurityService.logoff();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,16 @@
|
||||
import { Injector, Pipe, PipeTransform } from "@angular/core";
|
||||
import { HttpClient, HttpHeaders } from "@angular/common/http";
|
||||
import { OidcSecurityService } from "angular-auth-oidc-client";
|
||||
|
||||
@Pipe({
|
||||
name: "auth"
|
||||
})
|
||||
export class AuthPipe implements PipeTransform
|
||||
{
|
||||
private oidcSecurity: OidcSecurityService;
|
||||
|
||||
constructor(private injector: Injector, private http: HttpClient) {}
|
||||
|
||||
async transform(uri: string): Promise<string>
|
||||
{
|
||||
if (this.oidcSecurity === undefined)
|
||||
this.oidcSecurity = this.injector.get(OidcSecurityService);
|
||||
const token: string = this.oidcSecurity.getAccessToken();
|
||||
const token: string = null;
|
||||
if (!token)
|
||||
return uri;
|
||||
const headers: HttpHeaders = new HttpHeaders({Authorization: "Bearer " + token});
|
||||
|
@ -6,23 +6,17 @@ import {
|
||||
HttpInterceptor
|
||||
} from "@angular/common/http";
|
||||
import { Observable } from "rxjs";
|
||||
import { OidcSecurityService } from "angular-auth-oidc-client";
|
||||
|
||||
@Injectable()
|
||||
export class AuthorizerInterceptor implements HttpInterceptor
|
||||
{
|
||||
private oidcSecurity: OidcSecurityService;
|
||||
|
||||
|
||||
constructor(private injector: Injector) {}
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
|
||||
{
|
||||
if (request.url.startsWith("http"))
|
||||
return next.handle(request);
|
||||
if (this.oidcSecurity === undefined)
|
||||
this.oidcSecurity = this.injector.get(OidcSecurityService);
|
||||
const token: string = this.oidcSecurity.getAccessToken();
|
||||
const token: string = null;
|
||||
if (token)
|
||||
request = request.clone({setHeaders: {Authorization: "Bearer " + token}});
|
||||
return next.handle(request);
|
||||
|
@ -13,7 +13,6 @@ import {
|
||||
import { MatSnackBar } from "@angular/material/snack-bar";
|
||||
import { DomSanitizer, Title } from "@angular/platform-browser";
|
||||
import { ActivatedRoute, Event, NavigationCancel, NavigationEnd, NavigationStart, Router } from "@angular/router";
|
||||
import { OidcSecurityService } from "angular-auth-oidc-client";
|
||||
import Hls from "hls.js";
|
||||
import { EpisodeService, ShowService } from "../../services/api.service";
|
||||
import { StartupService } from "../../services/startup.service";
|
||||
@ -160,7 +159,6 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
|
||||
|
||||
private subtitlesManager: SubtitlesOctopus;
|
||||
private hlsPlayer: Hls = new Hls();
|
||||
private oidcSecurity: OidcSecurityService;
|
||||
constructor(private route: ActivatedRoute,
|
||||
private snackBar: MatSnackBar,
|
||||
private title: Title,
|
||||
@ -242,11 +240,9 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
|
||||
|
||||
ngAfterViewInit(): void
|
||||
{
|
||||
if (this.oidcSecurity === undefined)
|
||||
this.oidcSecurity = this.injector.get(OidcSecurityService);
|
||||
this.hlsPlayer.config.xhrSetup = xhr =>
|
||||
{
|
||||
const token: string = this.oidcSecurity.getAccessToken();
|
||||
const token: string = null;
|
||||
if (token)
|
||||
xhr.setRequestHeader("Authorization", "Bearer " + token);
|
||||
};
|
||||
|
4663
front/yarn.lock
4663
front/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -16,21 +16,16 @@
|
||||
// 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.DTO
|
||||
{
|
||||
/// <summary>
|
||||
/// A model to represent an otac request
|
||||
/// </summary>
|
||||
public class OtacRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The One Time Access Code
|
||||
/// </summary>
|
||||
public string Otac { get; set; }
|
||||
using System;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Permissions
|
||||
{
|
||||
/// <summary>
|
||||
/// Should the user stay logged
|
||||
/// The annotated route can only be accessed by a logged in user.
|
||||
/// </summary>
|
||||
public bool StayLoggedIn { get; set; }
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||
public class UserOnlyAttribute : Attribute
|
||||
{
|
||||
// TODO: Implement a Filter Attribute to make this work. For now, this attribute is only useful as documentation.
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
{
|
||||
@ -44,6 +45,7 @@ namespace Kyoo.Abstractions.Models
|
||||
/// <summary>
|
||||
/// The user password (hashed, it can't be read like that). The hashing format is implementation defined.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@ -54,6 +56,7 @@ namespace Kyoo.Abstractions.Models
|
||||
/// <summary>
|
||||
/// Arbitrary extra data that can be used by specific authentication implementations.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public Dictionary<string, string> ExtraData { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -62,11 +65,13 @@ namespace Kyoo.Abstractions.Models
|
||||
/// <summary>
|
||||
/// The list of shows the user has finished.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public ICollection<Show> Watched { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of episodes the user is watching (stopped in progress or the next episode of the show)
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public ICollection<WatchedEpisode> CurrentlyWatching { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -31,25 +31,30 @@ namespace Kyoo.Abstractions.Models.Utils
|
||||
/// </summary>
|
||||
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>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for main resources of kyoo.
|
||||
/// </summary>
|
||||
public const string ResourcesGroup = "0:Resources";
|
||||
public const string ResourcesGroup = "1:Resources";
|
||||
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>.
|
||||
/// It should be used for sub resources of kyoo that help define the main resources.
|
||||
/// </summary>
|
||||
public const string MetadataGroup = "1:Metadata";
|
||||
public const string MetadataGroup = "2:Metadata";
|
||||
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints useful for playback.
|
||||
/// </summary>
|
||||
public const string WatchGroup = "2:Watch";
|
||||
public const string WatchGroup = "3:Watch";
|
||||
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by admins.
|
||||
/// </summary>
|
||||
public const string AdminGroup = "3:Admin";
|
||||
public const string AdminGroup = "4:Admin";
|
||||
}
|
||||
}
|
||||
|
@ -18,28 +18,16 @@
|
||||
|
||||
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 SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Kyoo.Authentication
|
||||
{
|
||||
@ -55,14 +43,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 ApiKeys).";
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, Type> Configuration => new()
|
||||
{
|
||||
{ AuthenticationOption.Path, typeof(AuthenticationOption) },
|
||||
{ PermissionOption.Path, typeof(PermissionOption) },
|
||||
{ CertificateOption.Path, typeof(CertificateOption) }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@ -71,130 +58,50 @@ namespace Kyoo.Authentication
|
||||
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>
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
/// <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>
|
||||
/// <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)
|
||||
public AuthenticationModule(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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();
|
||||
builder.RegisterType<TokenController>().As<ITokenController>().SingleInstance();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(IServiceCollection services)
|
||||
{
|
||||
Uri publicUrl = _configuration.GetPublicUrl();
|
||||
|
||||
if (_environment.IsDevelopment())
|
||||
IdentityModelEventSource.ShowPII = true;
|
||||
|
||||
services.AddControllers();
|
||||
AuthenticationOption jwt = ConfigurationBinder.Get<AuthenticationOption>(
|
||||
_configuration.GetSection(AuthenticationOption.Path)
|
||||
);
|
||||
|
||||
// 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))
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IStartupAction> ConfigureSteps => new IStartupAction[]
|
||||
{
|
||||
SA.New<IApplicationBuilder>(app =>
|
||||
{
|
||||
PhysicalFileProvider provider = new(Path.Combine(
|
||||
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!,
|
||||
"login"));
|
||||
app.UseDefaultFiles(new DefaultFilesOptions
|
||||
{
|
||||
RequestPath = new PathString("/login"),
|
||||
FileProvider = provider,
|
||||
RedirectToAppendTrailingSlash = true
|
||||
});
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
RequestPath = new PathString("/login"),
|
||||
FileProvider = provider
|
||||
});
|
||||
}, 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)
|
||||
SA.New<IApplicationBuilder>(app => app.UseAuthentication(), SA.Authentication),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
54
src/Kyoo.Authentication/Controllers/ITokenController.cs
Normal file
54
src/Kyoo.Authentication/Controllers/ITokenController.cs
Normal 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 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(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(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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -72,7 +72,7 @@ namespace Kyoo.Authentication
|
||||
/// <summary>
|
||||
/// The permission to validate.
|
||||
/// </summary>
|
||||
private readonly string _permission;
|
||||
private readonly string? _permission;
|
||||
|
||||
/// <summary>
|
||||
/// The kind of permission needed.
|
||||
@ -96,7 +96,10 @@ namespace Kyoo.Authentication
|
||||
/// <param name="kind">The kind of permission needed.</param>
|
||||
/// <param name="group">The group of the permission.</param>
|
||||
/// <param name="options">The option containing default values.</param>
|
||||
public PermissionValidatorFilter(string permission, Kind kind, Group group,
|
||||
public PermissionValidatorFilter(
|
||||
string permission,
|
||||
Kind kind,
|
||||
Group group,
|
||||
IOptionsMonitor<PermissionOption> options)
|
||||
{
|
||||
_permission = permission;
|
||||
@ -133,7 +136,7 @@ namespace Kyoo.Authentication
|
||||
/// <inheritdoc />
|
||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
string permission = _permission;
|
||||
string? permission = _permission;
|
||||
Kind? kind = _kind;
|
||||
|
||||
if (permission == null || kind == null)
|
||||
|
144
src/Kyoo.Authentication/Controllers/TokenController.cs
Normal file
144
src/Kyoo.Authentication/Controllers/TokenController.cs
Normal file
@ -0,0 +1,144 @@
|
||||
// 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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -16,12 +16,10 @@
|
||||
// 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 IdentityModel;
|
||||
using IdentityServer4;
|
||||
using Kyoo.Abstractions.Models;
|
||||
|
||||
namespace Kyoo.Authentication
|
||||
{
|
||||
@ -30,35 +28,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>
|
||||
@ -66,7 +35,8 @@ namespace Kyoo.Authentication
|
||||
/// <returns>The list of permissions</returns>
|
||||
public static ICollection<string> GetPermissions(this ClaimsPrincipal user)
|
||||
{
|
||||
return user.Claims.FirstOrDefault(x => x.Type == "permissions")?.Value.Split(',');
|
||||
return user.Claims.FirstOrDefault(x => x.Type == "permissions")?.Value.Split(',')
|
||||
?? Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,23 +2,15 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<LoginRoot>../Kyoo.WebLogin/</LoginRoot>
|
||||
<Nullable>enable</Nullable>
|
||||
</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.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
|
||||
<ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="$(LoginRoot)**;" />
|
||||
<Content Include="$(LoginRoot)**" Visible="false">
|
||||
<Link>login/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -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/>.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Kyoo.Authentication.Models.DTO
|
||||
{
|
||||
/// <summary>
|
||||
/// A model only used on account update requests.
|
||||
/// </summary>
|
||||
public class AccountUpdateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The new email address of the user
|
||||
/// </summary>
|
||||
[EmailAddress(ErrorMessage = "The email is invalid.")]
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The new username of the user.
|
||||
/// </summary>
|
||||
[MinLength(4, ErrorMessage = "The username must have at least 4 characters")]
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The picture icon.
|
||||
/// </summary>
|
||||
public IFormFile Picture { get; set; }
|
||||
}
|
||||
}
|
@ -34,13 +34,14 @@ namespace Kyoo.Authentication.Models.DTO
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should the user stay logged in? If true a cookie will be put.
|
||||
/// Initializes a new instance of the <see cref="LoginRequest"/> class.
|
||||
/// </summary>
|
||||
public bool StayLoggedIn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The return url of the login flow.
|
||||
/// </summary>
|
||||
public string ReturnURL { get; set; }
|
||||
/// <param name="username">The user's username.</param>
|
||||
/// <param name="password">The user's password.</param>
|
||||
public LoginRequest(string username, string password)
|
||||
{
|
||||
Username = username;
|
||||
Password = password;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +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.DTO
|
||||
{
|
||||
/// <summary>
|
||||
/// A one time access token
|
||||
/// </summary>
|
||||
public class OtacResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The One Time Access Token that allow one to connect to an account without typing a password or without
|
||||
/// any kind of verification. This is valid only one time and only for a short period of time.
|
||||
/// </summary>
|
||||
public string OTAC { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="OtacResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="otac">The one time access token.</param>
|
||||
public OtacResponse(string otac)
|
||||
{
|
||||
OTAC = otac;
|
||||
}
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Kyoo.Abstractions.Models;
|
||||
using Kyoo.Utils;
|
||||
using BCryptNet = BCrypt.Net.BCrypt;
|
||||
|
||||
namespace Kyoo.Authentication.Models.DTO
|
||||
{
|
||||
@ -43,9 +44,22 @@ namespace Kyoo.Authentication.Models.DTO
|
||||
/// <summary>
|
||||
/// The user's password.
|
||||
/// </summary>
|
||||
[MinLength(8, ErrorMessage = "The password must have at least {1} characters")]
|
||||
[MinLength(4, ErrorMessage = "The password must have at least {1} characters")]
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegisterRequest"/> class.
|
||||
/// </summary>
|
||||
/// <param name="email">The user email address.</param>
|
||||
/// <param name="username">The user's username.</param>
|
||||
/// <param name="password">The user's password.</param>
|
||||
public RegisterRequest(string email, string username, string password)
|
||||
{
|
||||
Email = email;
|
||||
Username = username;
|
||||
Password = password;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert this register request to a new <see cref="User"/> class.
|
||||
/// </summary>
|
||||
@ -56,7 +70,7 @@ namespace Kyoo.Authentication.Models.DTO
|
||||
{
|
||||
Slug = Utility.ToSlug(Username),
|
||||
Username = Username,
|
||||
Password = Password,
|
||||
Password = BCryptNet.HashPassword(Password),
|
||||
Email = Email,
|
||||
ExtraData = new Dictionary<string, string>()
|
||||
};
|
||||
|
@ -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()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
72
src/Kyoo.Authentication/Models/JwtToken.cs
Normal file
72
src/Kyoo.Authentication/Models/JwtToken.cs
Normal file
@ -0,0 +1,72 @@
|
||||
// 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";
|
||||
|
||||
/// <summary>
|
||||
/// The access token used to authorize requests.
|
||||
/// </summary>
|
||||
[JsonProperty("access_token")]
|
||||
[JsonPropertyName("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
/// <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>
|
||||
/// When the access token will expire. After this time, the refresh token should be used to retrieve.
|
||||
/// a new token.cs
|
||||
/// </summary>
|
||||
[JsonProperty("expire_in")]
|
||||
[JsonPropertyName("expire_in")]
|
||||
public TimeSpan ExpireIn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JwtToken"/> class.
|
||||
/// </summary>
|
||||
/// <param name="accessToken">The access token used to authorize requests.</param>
|
||||
/// <param name="refreshToken">The refresh token to retrieve a new access token.</param>
|
||||
/// <param name="expireIn">When the access token will expire.</param>
|
||||
public JwtToken(string accessToken, string refreshToken, TimeSpan expireIn)
|
||||
{
|
||||
AccessToken = accessToken;
|
||||
RefreshToken = refreshToken;
|
||||
ExpireIn = expireIn;
|
||||
}
|
||||
}
|
||||
}
|
@ -29,18 +29,23 @@ namespace Kyoo.Authentication.Models
|
||||
public const string Path = "authentication";
|
||||
|
||||
/// <summary>
|
||||
/// The options for certificates
|
||||
/// The default jwt secret.
|
||||
/// </summary>
|
||||
public CertificateOption Certificate { get; set; }
|
||||
public const string DefaultSecret = "4c@mraGB!KRfF@kpS8739y9FcHemKxBsqqxLbdR?";
|
||||
|
||||
/// <summary>
|
||||
/// The secret used to encrypt the jwt.
|
||||
/// </summary>
|
||||
public string Secret { get; set; } = DefaultSecret;
|
||||
|
||||
/// <summary>
|
||||
/// Options for permissions
|
||||
/// </summary>
|
||||
public PermissionOption Permissions { get; set; }
|
||||
public PermissionOption Permissions { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Root path of user's profile pictures.
|
||||
/// </summary>
|
||||
public string ProfilePicturePath { get; set; }
|
||||
public string ProfilePicturePath { get; set; } = "users/";
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -31,11 +31,11 @@ namespace Kyoo.Authentication.Models
|
||||
/// <summary>
|
||||
/// The default permissions that will be given to a non-connected user.
|
||||
/// </summary>
|
||||
public string[] Default { get; set; }
|
||||
public string[] Default { get; set; } = new[] { "overall.read", "overall.write" };
|
||||
|
||||
/// <summary>
|
||||
/// Permissions applied to a new user.
|
||||
/// </summary>
|
||||
public string[] NewUser { get; set; }
|
||||
public string[] NewUser { get; set; } = new[] { "overall.read", "overall.write" };
|
||||
}
|
||||
}
|
||||
|
@ -1,263 +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.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.Models.DTO;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using static Kyoo.Abstractions.Models.Utils.Constants;
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
/// TODO document this well.
|
||||
[Route("api/accounts")]
|
||||
[Route("api/account", Order = AlternativeRoute)]
|
||||
[ApiController]
|
||||
[ApiDefinition("Account")]
|
||||
public class AccountApi : Controller, IProfileService
|
||||
{
|
||||
/// <summary>
|
||||
/// The repository to handle users.
|
||||
/// </summary>
|
||||
private readonly IUserRepository _users;
|
||||
|
||||
/// <summary>
|
||||
/// A file manager to send profile pictures
|
||||
/// </summary>
|
||||
private readonly IFileSystem _files;
|
||||
|
||||
/// <summary>
|
||||
/// Options about authentication. Those options are monitored and reloads are supported.
|
||||
/// </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)
|
||||
{
|
||||
_users = users;
|
||||
_files = files;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Register a new user and return a OTAC to connect to it.
|
||||
/// </remarks>
|
||||
/// <param name="request">The DTO register request</param>
|
||||
/// <returns>A OTAC to connect to this new account</returns>
|
||||
[HttpPost("register")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(RequestError))]
|
||||
public async Task<ActionResult<OtacResponse>> 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
|
||||
{
|
||||
await _users.Create(user);
|
||||
}
|
||||
catch (DuplicatedItemException)
|
||||
{
|
||||
return Conflict(new RequestError("A user with this name already exists"));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!stayLogged)
|
||||
return null;
|
||||
return new AuthenticationProperties
|
||||
{
|
||||
IsPersistent = true,
|
||||
ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(1)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Login
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Login the current session.
|
||||
/// </remarks>
|
||||
/// <param name="login">The DTO login request</param>
|
||||
/// <returns>TODO</returns>
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login([FromBody] LoginRequest login)
|
||||
{
|
||||
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 });
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign out an user
|
||||
/// </summary>
|
||||
/// <returns>TODO</returns>
|
||||
[HttpGet("logout")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await HttpContext.SignOutAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
|
||||
{
|
||||
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)));
|
||||
}
|
||||
|
||||
/// <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]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<User>> 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);
|
||||
}
|
||||
|
||||
/// <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>();
|
||||
}
|
||||
}
|
||||
}
|
254
src/Kyoo.Authentication/Views/AuthApi.cs
Normal file
254
src/Kyoo.Authentication/Views/AuthApi.cs
Normal file
@ -0,0 +1,254 @@
|
||||
// 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.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.Permissions;
|
||||
using Kyoo.Abstractions.Models.Utils;
|
||||
using Kyoo.Authentication.Models.DTO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
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>
|
||||
/// Create a new Forbidden result from an object.
|
||||
/// </summary>
|
||||
/// <param name="value">The json value to output on the response.</param>
|
||||
/// <returns>A new forbidden result with the given json object.</returns>
|
||||
public static ObjectResult Forbid(object value)
|
||||
{
|
||||
return new ObjectResult(value) { StatusCode = StatusCodes.Status403Forbidden };
|
||||
}
|
||||
|
||||
/// <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="403">The user and password does not match.</response>
|
||||
[HttpPost("login")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, 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 Forbid(new RequestError("The user and password does not match."));
|
||||
|
||||
return new JwtToken(
|
||||
_token.CreateAccessToken(user, out TimeSpan expireIn),
|
||||
await _token.CreateRefreshToken(user),
|
||||
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(
|
||||
_token.CreateAccessToken(user, out TimeSpan expireIn),
|
||||
await _token.CreateRefreshToken(user),
|
||||
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="403">The given refresh token is invalid.</response>
|
||||
[HttpGet("refresh")]
|
||||
[UserOnly]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, 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(
|
||||
_token.CreateAccessToken(user, out TimeSpan expireIn),
|
||||
await _token.CreateRefreshToken(user),
|
||||
expireIn
|
||||
);
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
return Forbid(new RequestError("Invalid refresh token."));
|
||||
}
|
||||
catch (SecurityTokenException ex)
|
||||
{
|
||||
return Forbid(new RequestError(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get authenticated user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Get information about the currently authenticated user. This can also be used to ensure that you are
|
||||
/// logged in.
|
||||
/// </remarks>
|
||||
/// <returns>The currently authenticated user.</returns>
|
||||
/// <response code="403">The given access token is invalid.</response>
|
||||
[HttpGet("me")]
|
||||
[UserOnly]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<User>> GetMe()
|
||||
{
|
||||
if (!int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out int userID))
|
||||
return Forbid();
|
||||
try
|
||||
{
|
||||
return await _users.Get(userID);
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Edit self
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Edit information about the currently authenticated user.
|
||||
/// </remarks>
|
||||
/// <param name="user">The new data for the current user.</param>
|
||||
/// <param name="resetOld">
|
||||
/// Should old properties of the resource be discarded or should null values considered as not changed?
|
||||
/// </param>
|
||||
/// <returns>The currently authenticated user after modifications.</returns>
|
||||
/// <response code="403">The given access token is invalid.</response>
|
||||
[HttpPut("me")]
|
||||
[UserOnly]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<User>> EditMe(User user, [FromQuery] bool resetOld = true)
|
||||
{
|
||||
if (!int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out int userID))
|
||||
return Forbid();
|
||||
try
|
||||
{
|
||||
user.ID = userID;
|
||||
return await _users.Edit(user, resetOld);
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete account
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Delete the current account.
|
||||
/// </remarks>
|
||||
/// <returns>The currently authenticated user after modifications.</returns>
|
||||
/// <response code="403">The given access token is invalid.</response>
|
||||
[HttpDelete("me")]
|
||||
[UserOnly]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<User>> DeleteMe()
|
||||
{
|
||||
if (!int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out int userID))
|
||||
return Forbid();
|
||||
try
|
||||
{
|
||||
await _users.Delete(userID);
|
||||
return NoContent();
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -118,6 +118,7 @@ namespace Kyoo.Core
|
||||
/// <inheritdoc />
|
||||
public void Configure(IServiceCollection services)
|
||||
{
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddTransient<IConfigureOptions<MvcNewtonsoftJsonOptions>, JsonOptions>();
|
||||
|
||||
services.AddMvcCore()
|
||||
|
@ -220,7 +220,7 @@ namespace Kyoo.Core.Api
|
||||
/// <response code="404">No item could be found with the given id or slug.</response>
|
||||
[HttpDelete("{identifier:id}")]
|
||||
[PartialPermission(Kind.Delete)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(Identifier identifier)
|
||||
{
|
||||
@ -236,7 +236,7 @@ namespace Kyoo.Core.Api
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -250,7 +250,7 @@ namespace Kyoo.Core.Api
|
||||
/// <response code="400">One or multiple filters are invalid.</response>
|
||||
[HttpDelete]
|
||||
[PartialPermission(Kind.Delete)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
||||
public async Task<IActionResult> Delete([FromQuery] Dictionary<string, string> where)
|
||||
{
|
||||
@ -263,7 +263,7 @@ namespace Kyoo.Core.Api
|
||||
return BadRequest(new RequestError(ex.Message));
|
||||
}
|
||||
|
||||
return Ok();
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ namespace Kyoo.Core.Api
|
||||
/// </summary>
|
||||
[Route("subtitles")]
|
||||
[Route("subtitle", Order = AlternativeRoute)]
|
||||
[PartialPermission(nameof(SubtitleApi))]
|
||||
[PartialPermission("subtitle")]
|
||||
[ApiController]
|
||||
[ApiDefinition("Subtitles", Group = WatchGroup)]
|
||||
public class SubtitleApi : ControllerBase
|
||||
|
@ -161,7 +161,8 @@ namespace Kyoo.Host.Generic
|
||||
.OrderByDescending(x => x.Priority);
|
||||
|
||||
using ILifetimeScope scope = container.BeginLifetimeScope(x =>
|
||||
x.RegisterInstance(app).SingleInstance().ExternallyOwned());
|
||||
x.RegisterInstance(app).SingleInstance().ExternallyOwned()
|
||||
);
|
||||
IServiceProvider provider = scope.Resolve<IServiceProvider>();
|
||||
foreach (IStartupAction step in steps)
|
||||
step.Run(provider);
|
||||
|
@ -60,17 +60,12 @@
|
||||
},
|
||||
|
||||
"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"]
|
||||
"default": ["overall.read", "overall.write"],
|
||||
"newUser": ["overall.read", "overall.write"]
|
||||
},
|
||||
"profilePicturePath": "users/",
|
||||
"clients": []
|
||||
"secret": "4c@mraGB!KRfF@kpS8740y9FcHemKxBsqqxLbdR?"
|
||||
},
|
||||
|
||||
"tvdb": {
|
||||
|
@ -16,6 +16,7 @@
|
||||
// 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.Reflection;
|
||||
@ -36,12 +37,19 @@ namespace Kyoo.Swagger
|
||||
public bool Process(OperationProcessorContext context)
|
||||
{
|
||||
context.OperationDescription.Operation.Security ??= new List<OpenApiSecurityRequirement>();
|
||||
OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes<PermissionAttribute>()
|
||||
OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes<UserOnlyAttribute>()
|
||||
.Aggregate(new OpenApiSecurityRequirement(), (agg, cur) =>
|
||||
{
|
||||
agg[nameof(Kyoo)] = Array.Empty<string>();
|
||||
return agg;
|
||||
});
|
||||
|
||||
perms = context.MethodInfo.GetCustomAttributes<PermissionAttribute>()
|
||||
.Aggregate(perms, (agg, cur) =>
|
||||
{
|
||||
ICollection<string> permissions = _GetPermissionsList(agg, cur.Group);
|
||||
permissions.Add($"{cur.Type}.{cur.Kind.ToString().ToLower()}");
|
||||
agg[cur.Group.ToString()] = permissions;
|
||||
agg[nameof(Kyoo)] = permissions;
|
||||
return agg;
|
||||
});
|
||||
|
||||
@ -61,7 +69,7 @@ namespace Kyoo.Swagger
|
||||
: cur.Kind;
|
||||
ICollection<string> permissions = _GetPermissionsList(agg, group);
|
||||
permissions.Add($"{type}.{kind.ToString().ToLower()}");
|
||||
agg[group.ToString()] = permissions;
|
||||
agg[nameof(Kyoo)] = permissions;
|
||||
return agg;
|
||||
});
|
||||
}
|
||||
@ -70,7 +78,7 @@ namespace Kyoo.Swagger
|
||||
return true;
|
||||
}
|
||||
|
||||
private ICollection<string> _GetPermissionsList(OpenApiSecurityRequirement security, Group group)
|
||||
private static ICollection<string> _GetPermissionsList(OpenApiSecurityRequirement security, Group group)
|
||||
{
|
||||
return security.TryGetValue(group.ToString(), out IEnumerable<string> perms)
|
||||
? perms.ToList()
|
||||
|
@ -120,35 +120,14 @@ namespace Kyoo.Swagger
|
||||
}));
|
||||
document.SchemaProcessors.Add(new ThumbnailProcessor());
|
||||
|
||||
document.AddSecurity("Kyoo", new OpenApiSecurityScheme
|
||||
document.AddSecurity(nameof(Kyoo), new OpenApiSecurityScheme
|
||||
{
|
||||
Type = OpenApiSecuritySchemeType.OpenIdConnect,
|
||||
OpenIdConnectUrl = "/.well-known/openid-configuration",
|
||||
Description = "You can login via an OIDC client, clients must be first registered in kyoo. " +
|
||||
"Documentation coming soon."
|
||||
Type = OpenApiSecuritySchemeType.Http,
|
||||
Scheme = "Bearer",
|
||||
BearerFormat = "JWT",
|
||||
Description = "The user's bearer"
|
||||
});
|
||||
document.OperationProcessors.Add(new OperationPermissionProcessor());
|
||||
// This does not respect the swagger's specification but it works for swaggerUi and ReDoc so honestly this will do.
|
||||
document.AddSecurity(Group.Overall.ToString(), new OpenApiSecurityScheme
|
||||
{
|
||||
ExtensionData = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "OpenID Connect or Api Key"
|
||||
},
|
||||
Description = "Kyoo's permissions work by groups. Permissions are attributed to " +
|
||||
"a specific group and if a user has a group permission, it will be the same as having every " +
|
||||
"permission in the group. For example, having overall.read gives you collections.read, " +
|
||||
"shows.read and so on."
|
||||
});
|
||||
document.AddSecurity(Group.Admin.ToString(), new OpenApiSecurityScheme
|
||||
{
|
||||
ExtensionData = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "OpenID Connect or Api Key"
|
||||
},
|
||||
Description = "The permission group used for administrative items like tasks, account management " +
|
||||
"and library creation."
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,90 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Kyoo - Login</title>
|
||||
<link rel="stylesheet" type="text/css" href="lib/bootstrap.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="login.css" />
|
||||
<link rel="stylesheet" type="text/css" href="material-icons.css" />
|
||||
<script src="lib/jquery.min.js"></script>
|
||||
<script src="lib/bootstrap.min.js"></script>
|
||||
</head>
|
||||
<body style="height: 100vh; align-items: center;" class="d-flex">
|
||||
<div class="container pb-5">
|
||||
<div class="card bg-dark mb-5">
|
||||
<div class="card-header bg-secondary text-white">
|
||||
<ul class="nav nav-tabs card-header-tabs navbar-dark" role="tablist" id="tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="login-tab" data-toggle="tab" href="#login" role="tab" aria-selected="true">Login</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#register" role="tab" aria-selected="false">Register</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body tab-content">
|
||||
<div class="tab-pane fade show active" id="login" role="tabpanel" aria-labelledby="login-tab">
|
||||
<div class="alert alert-danger" role="alert" id="login-error" style="display: none;"></div>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="login-username">Username</label>
|
||||
<input autocomplete="username" class="form-control" id="login-username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="login-password">Password</label>
|
||||
<div class="input-group">
|
||||
<input autocomplete="current-password" type="password" class="form-control" id="login-password">
|
||||
<div class="input-group-append password-visibility">
|
||||
<a class="input-group-text" href="#"><i class="material-icons">visibility</i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" id="remember-me">
|
||||
<label class="form-check-label" for="remember-me">Remember me</label>
|
||||
</div>
|
||||
<button id="login-btn" type="submit" class="btn" style="background-color: #e23c00;">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="register" role="tabpanel" aria-labelledby="register-tab">
|
||||
<div class="alert alert-danger" role="alert" id="register-error" style="display: none;"></div>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="register-email">Email</label>
|
||||
<input type="email" class="form-control" id="register-email">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="register-username">Username</label>
|
||||
<input autocomplete="username" class="form-control" id="register-username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="register-password">Password</label>
|
||||
<div class="input-group">
|
||||
<input autocomplete="new-password" type="password" class="form-control" id="register-password">
|
||||
<div class="input-group-append password-visibility">
|
||||
<a class="input-group-text" href="#"><i class="material-icons">visibility</i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="register-password-confirm">Confirm Password</label>
|
||||
<div class="input-group">
|
||||
<input autocomplete="new-password" type="password" class="form-control" id="register-password-confirm">
|
||||
<div class="input-group-append password-visibility">
|
||||
<a class="input-group-text" href="#"><i class="material-icons">visibility</i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" id="stay-logged-in">
|
||||
<label class="form-check-label" for="stay-logged-in">Stay logged in</label>
|
||||
</div>
|
||||
<button id="register-btn" type="submit" class="btn" style="background-color: #e23c00;">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="login.js"></script>
|
||||
</body>
|
||||
</html>
|
1
src/Kyoo.WebLogin/lib/bootstrap.min.css
vendored
1
src/Kyoo.WebLogin/lib/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
7
src/Kyoo.WebLogin/lib/bootstrap.min.js
vendored
7
src/Kyoo.WebLogin/lib/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
2
src/Kyoo.WebLogin/lib/jquery.min.js
vendored
2
src/Kyoo.WebLogin/lib/jquery.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,16 +0,0 @@
|
||||
.nav-tabs .nav-link, a
|
||||
{
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-item.show .nav-link, .nav-tabs .nav-link.active
|
||||
{
|
||||
background-color: #343a40;
|
||||
color: white;
|
||||
border-color: #343a40;
|
||||
}
|
||||
|
||||
.form-control:focus
|
||||
{
|
||||
box-shadow: none;
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
$("#tabs a").on("click", function (e)
|
||||
{
|
||||
e.preventDefault();
|
||||
$(this).tab("show");
|
||||
});
|
||||
|
||||
$(".password-visibility a").on("click", function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
let password = $(this).parent().siblings("input");
|
||||
let toggle = $(this).children("i");
|
||||
|
||||
if (password.attr("type") === "text")
|
||||
{
|
||||
password.attr("type", "password");
|
||||
toggle.text("visibility");
|
||||
}
|
||||
else
|
||||
{
|
||||
toggle.text("visibility_off");
|
||||
password.attr("type", "text");
|
||||
}
|
||||
});
|
||||
|
||||
$("#login-btn").on("click", function (e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
let user = {
|
||||
username: $("#login-username")[0].value,
|
||||
password: $("#login-password")[0].value,
|
||||
stayLoggedIn: $("#remember-me")[0].checked
|
||||
};
|
||||
|
||||
$.ajax(
|
||||
{
|
||||
url: "/api/account/login",
|
||||
type: "POST",
|
||||
contentType: 'application/json;charset=UTF-8',
|
||||
data: JSON.stringify(user),
|
||||
success: function ()
|
||||
{
|
||||
let returnUrl = new URLSearchParams(window.location.search).get("ReturnUrl");
|
||||
|
||||
if (returnUrl == null)
|
||||
window.location.href = "/unauthorized";
|
||||
else
|
||||
window.location.href = returnUrl;
|
||||
},
|
||||
error: function(xhr)
|
||||
{
|
||||
let error = $("#login-error");
|
||||
error.show();
|
||||
error.text(JSON.parse(xhr.responseText)[0].description);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#register-btn").on("click", function (e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
let user = {
|
||||
email: $("#register-email")[0].value,
|
||||
username: $("#register-username")[0].value,
|
||||
password: $("#register-password")[0].value
|
||||
};
|
||||
|
||||
if (user.password !== $("#register-password-confirm")[0].value)
|
||||
{
|
||||
let error = $("#register-error");
|
||||
error.show();
|
||||
error.text("Passwords don't match.");
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax(
|
||||
{
|
||||
url: "/api/account/register",
|
||||
type: "POST",
|
||||
contentType: 'application/json;charset=UTF-8',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify(user),
|
||||
success: function(res)
|
||||
{
|
||||
useOtac(res.otac);
|
||||
},
|
||||
error: function(xhr)
|
||||
{
|
||||
let error = $("#register-error");
|
||||
error.show();
|
||||
error.html(Object.values(JSON.parse(xhr.responseText).errors).map(x => x[0]).join("<br/>"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function useOtac(otac)
|
||||
{
|
||||
$.ajax(
|
||||
{
|
||||
url: "/api/account/otac-login",
|
||||
type: "POST",
|
||||
contentType: 'application/json;charset=UTF-8',
|
||||
data: JSON.stringify({otac: otac, stayLoggedIn: $("#stay-logged-in")[0].checked}),
|
||||
success: function()
|
||||
{
|
||||
let returnUrl = new URLSearchParams(window.location.search).get("ReturnUrl");
|
||||
|
||||
if (returnUrl == null)
|
||||
window.location.href = "/unauthorized";
|
||||
else
|
||||
window.location.href = returnUrl;
|
||||
},
|
||||
error: function(xhr)
|
||||
{
|
||||
let error = $("#register-error");
|
||||
error.show();
|
||||
error.text(JSON.parse(xhr.responseText)[0].description);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
let otac = new URLSearchParams(window.location.search).get("otac");
|
||||
if (otac != null)
|
||||
useOtac(otac);
|
@ -1,36 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(/iconfont/MaterialIcons-Regular.eot); /* For IE6-8 */
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url(/iconfont/MaterialIcons-Regular.woff2) format('woff2'),
|
||||
url(/iconfont/MaterialIcons-Regular.woff) format('woff'),
|
||||
url(/iconfont/MaterialIcons-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px; /* Preferred icon size */
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
/* Support for Firefox. */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
/* Support for IE. */
|
||||
font-feature-settings: 'liga';
|
||||
}
|
1
tests/robot/.gitignore
vendored
Normal file
1
tests/robot/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
venv/
|
83
tests/robot/auth/auth.robot
Normal file
83
tests/robot/auth/auth.robot
Normal file
@ -0,0 +1,83 @@
|
||||
*** Settings ***
|
||||
Documentation Tests of the /auth route.
|
||||
... Ensures that the user can authenticate on kyoo.
|
||||
|
||||
Resource ../rest.resource
|
||||
|
||||
|
||||
*** Keywords ***
|
||||
Login
|
||||
[Documentation] Shortcut to login with the given username for future requests
|
||||
[Arguments] ${username}
|
||||
&{res}= POST /auth/login {"username": "${username}", "password": "password-${username}"}
|
||||
Output
|
||||
Integer response status 200
|
||||
String response body access_token
|
||||
Set Headers {"Authorization": "Bearer ${res.body.access_token}"}
|
||||
|
||||
Register
|
||||
[Documentation] Shortcut to register with the given username for future requests
|
||||
[Arguments] ${username}
|
||||
&{res}= POST
|
||||
... /auth/register
|
||||
... {"username": "${username}", "password": "password-${username}", "email": "${username}@kyoo.moe"}
|
||||
Output
|
||||
Integer response status 200
|
||||
String response body access_token
|
||||
Set Headers {"Authorization": "Bearer ${res.body.access_token}"}
|
||||
|
||||
Logout
|
||||
[Documentation] Logout the current user, only the local client is affected.
|
||||
Set Headers {"Authorization": ""}
|
||||
|
||||
|
||||
*** Test Cases ***
|
||||
Me cant be accessed without an account
|
||||
Get /auth/me
|
||||
Output
|
||||
Integer response status 403
|
||||
|
||||
Bad Account
|
||||
[Documentation] Login fails if user does not exist
|
||||
POST /auth/login {"username": "i-don-t-exist", "password": "pass"}
|
||||
Output
|
||||
Integer response status 403
|
||||
|
||||
Register
|
||||
[Documentation] Create a new user and login in it
|
||||
Register user-1
|
||||
[Teardown] DELETE /auth/me
|
||||
|
||||
Register Duplicates
|
||||
[Documentation] If two users tries to register with the same username, it fails
|
||||
Register user-duplicate
|
||||
# We can't use the `Register` keyword because it assert for success
|
||||
POST /auth/register {"username": "user-duplicate", "password": "pass", "email": "mail@kyoo.moe"}
|
||||
Output
|
||||
Integer response status 409
|
||||
[Teardown] DELETE /auth/me
|
||||
|
||||
Delete Account
|
||||
[Documentation] Check if a user can delete it's account
|
||||
Register I-should-be-deleted
|
||||
DELETE /auth/me
|
||||
Output
|
||||
Integer response status 204
|
||||
|
||||
Login
|
||||
[Documentation] Create a new user and login in it
|
||||
Register login-user
|
||||
${res}= GET /auth/me
|
||||
Output
|
||||
Integer response status 200
|
||||
String response body username login-user
|
||||
|
||||
Logout
|
||||
Login login-user
|
||||
${me}= Get /auth/me
|
||||
Output
|
||||
Output ${me}
|
||||
Should Be Equal As Strings ${res["body"]} ${me["body"]}
|
||||
|
||||
[Teardown] DELETE /auth/me
|
||||
|
4
tests/robot/pyproject.toml
Normal file
4
tests/robot/pyproject.toml
Normal file
@ -0,0 +1,4 @@
|
||||
[tool.robotidy]
|
||||
configure = [
|
||||
"MergeAndOrderSections:order=comments,settings,keywords,variables,testcases"
|
||||
]
|
2
tests/robot/requirements.txt
Normal file
2
tests/robot/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
robotframework
|
||||
RESTinstance
|
4
tests/robot/rest.resource
Normal file
4
tests/robot/rest.resource
Normal file
@ -0,0 +1,4 @@
|
||||
*** Settings ***
|
||||
Documentation Common things to handle rest requests
|
||||
|
||||
Library REST http://localhost:5000/api
|
Loading…
x
Reference in New Issue
Block a user