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
|
csharp_indent_switch_labels = true
|
||||||
# Modifiers
|
# Modifiers
|
||||||
dotnet_style_readonly_field = true:suggestion
|
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
|
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
|
||||||
# Naming style
|
# Naming style
|
||||||
dotnet_naming_symbols.privates.applicable_kinds = property,method,event,delegate
|
dotnet_naming_symbols.privates.applicable_kinds = property,method,event,delegate
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
TVDB__APIKEY=
|
TVDB__APIKEY=
|
||||||
THEMOVIEDB__APIKEY=
|
THEMOVIEDB__APIKEY=
|
||||||
|
AUTHENTICATION_SECRET=
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -356,3 +356,4 @@ healthchecksdb
|
|||||||
/Kyoo/TheTVDB-Credentials.json
|
/Kyoo/TheTVDB-Credentials.json
|
||||||
|
|
||||||
.vscode
|
.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.
|
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
|
## 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.
|
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
|
## Skipping parts
|
||||||
|
@ -26,7 +26,6 @@
|
|||||||
"@angular/platform-browser": "^13.0.2",
|
"@angular/platform-browser": "^13.0.2",
|
||||||
"@angular/platform-browser-dynamic": "^13.0.2",
|
"@angular/platform-browser-dynamic": "^13.0.2",
|
||||||
"@angular/router": "^13.0.2",
|
"@angular/router": "^13.0.2",
|
||||||
"angular-auth-oidc-client": "^13.0.0",
|
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"detect-browser": "^5.2.1",
|
"detect-browser": "^5.2.1",
|
||||||
"hls.js": "^1.1.1",
|
"hls.js": "^1.1.1",
|
||||||
|
@ -16,7 +16,6 @@ import { MatSliderModule } from "@angular/material/slider";
|
|||||||
import { MatTabsModule } from "@angular/material/tabs";
|
import { MatTabsModule } from "@angular/material/tabs";
|
||||||
import { MatTooltipModule } from "@angular/material/tooltip";
|
import { MatTooltipModule } from "@angular/material/tooltip";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
import { AuthModule as OidcModule, LogLevel } from "angular-auth-oidc-client";
|
|
||||||
import { tap } from "rxjs/operators";
|
import { tap } from "rxjs/operators";
|
||||||
import { AccountComponent } from "./account/account.component";
|
import { AccountComponent } from "./account/account.component";
|
||||||
import { LogoutComponent } from "./logout/logout.component";
|
import { LogoutComponent } from "./logout/logout.component";
|
||||||
@ -48,25 +47,6 @@ import { UnauthorizedComponent } from "./unauthorized/unauthorized.component";
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
MatCheckboxModule,
|
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
|
RouterModule
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import { LoginResponse, OidcSecurityService } from "angular-auth-oidc-client";
|
|
||||||
import { Account } from "../models/account";
|
import { Account } from "../models/account";
|
||||||
import { HttpClient } from "@angular/common/http";
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: "root"
|
providedIn: "root"
|
||||||
@ -11,37 +9,15 @@ export class AuthService
|
|||||||
isAuthenticated: boolean = false;
|
isAuthenticated: boolean = false;
|
||||||
account: Account = null;
|
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
|
login(): void
|
||||||
{
|
{
|
||||||
this.oidcSecurityService.authorize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logout(): void
|
logout(): void
|
||||||
{
|
{
|
||||||
this.http.get("api/account/logout").subscribe(() =>
|
|
||||||
{
|
|
||||||
this.oidcSecurityService.logoff();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,16 @@
|
|||||||
import { Injector, Pipe, PipeTransform } from "@angular/core";
|
import { Injector, Pipe, PipeTransform } from "@angular/core";
|
||||||
import { HttpClient, HttpHeaders } from "@angular/common/http";
|
import { HttpClient, HttpHeaders } from "@angular/common/http";
|
||||||
import { OidcSecurityService } from "angular-auth-oidc-client";
|
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: "auth"
|
name: "auth"
|
||||||
})
|
})
|
||||||
export class AuthPipe implements PipeTransform
|
export class AuthPipe implements PipeTransform
|
||||||
{
|
{
|
||||||
private oidcSecurity: OidcSecurityService;
|
|
||||||
|
|
||||||
constructor(private injector: Injector, private http: HttpClient) {}
|
constructor(private injector: Injector, private http: HttpClient) {}
|
||||||
|
|
||||||
async transform(uri: string): Promise<string>
|
async transform(uri: string): Promise<string>
|
||||||
{
|
{
|
||||||
if (this.oidcSecurity === undefined)
|
const token: string = null;
|
||||||
this.oidcSecurity = this.injector.get(OidcSecurityService);
|
|
||||||
const token: string = this.oidcSecurity.getAccessToken();
|
|
||||||
if (!token)
|
if (!token)
|
||||||
return uri;
|
return uri;
|
||||||
const headers: HttpHeaders = new HttpHeaders({Authorization: "Bearer " + token});
|
const headers: HttpHeaders = new HttpHeaders({Authorization: "Bearer " + token});
|
||||||
|
@ -6,23 +6,17 @@ import {
|
|||||||
HttpInterceptor
|
HttpInterceptor
|
||||||
} from "@angular/common/http";
|
} from "@angular/common/http";
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
import { OidcSecurityService } from "angular-auth-oidc-client";
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthorizerInterceptor implements HttpInterceptor
|
export class AuthorizerInterceptor implements HttpInterceptor
|
||||||
{
|
{
|
||||||
private oidcSecurity: OidcSecurityService;
|
|
||||||
|
|
||||||
|
|
||||||
constructor(private injector: Injector) {}
|
constructor(private injector: Injector) {}
|
||||||
|
|
||||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
|
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
|
||||||
{
|
{
|
||||||
if (request.url.startsWith("http"))
|
if (request.url.startsWith("http"))
|
||||||
return next.handle(request);
|
return next.handle(request);
|
||||||
if (this.oidcSecurity === undefined)
|
const token: string = null;
|
||||||
this.oidcSecurity = this.injector.get(OidcSecurityService);
|
|
||||||
const token: string = this.oidcSecurity.getAccessToken();
|
|
||||||
if (token)
|
if (token)
|
||||||
request = request.clone({setHeaders: {Authorization: "Bearer " + token}});
|
request = request.clone({setHeaders: {Authorization: "Bearer " + token}});
|
||||||
return next.handle(request);
|
return next.handle(request);
|
||||||
|
@ -13,7 +13,6 @@ import {
|
|||||||
import { MatSnackBar } from "@angular/material/snack-bar";
|
import { MatSnackBar } from "@angular/material/snack-bar";
|
||||||
import { DomSanitizer, Title } from "@angular/platform-browser";
|
import { DomSanitizer, Title } from "@angular/platform-browser";
|
||||||
import { ActivatedRoute, Event, NavigationCancel, NavigationEnd, NavigationStart, Router } from "@angular/router";
|
import { ActivatedRoute, Event, NavigationCancel, NavigationEnd, NavigationStart, Router } from "@angular/router";
|
||||||
import { OidcSecurityService } from "angular-auth-oidc-client";
|
|
||||||
import Hls from "hls.js";
|
import Hls from "hls.js";
|
||||||
import { EpisodeService, ShowService } from "../../services/api.service";
|
import { EpisodeService, ShowService } from "../../services/api.service";
|
||||||
import { StartupService } from "../../services/startup.service";
|
import { StartupService } from "../../services/startup.service";
|
||||||
@ -160,7 +159,6 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
|
|||||||
|
|
||||||
private subtitlesManager: SubtitlesOctopus;
|
private subtitlesManager: SubtitlesOctopus;
|
||||||
private hlsPlayer: Hls = new Hls();
|
private hlsPlayer: Hls = new Hls();
|
||||||
private oidcSecurity: OidcSecurityService;
|
|
||||||
constructor(private route: ActivatedRoute,
|
constructor(private route: ActivatedRoute,
|
||||||
private snackBar: MatSnackBar,
|
private snackBar: MatSnackBar,
|
||||||
private title: Title,
|
private title: Title,
|
||||||
@ -242,11 +240,9 @@ export class PlayerComponent implements OnInit, OnDestroy, AfterViewInit
|
|||||||
|
|
||||||
ngAfterViewInit(): void
|
ngAfterViewInit(): void
|
||||||
{
|
{
|
||||||
if (this.oidcSecurity === undefined)
|
|
||||||
this.oidcSecurity = this.injector.get(OidcSecurityService);
|
|
||||||
this.hlsPlayer.config.xhrSetup = xhr =>
|
this.hlsPlayer.config.xhrSetup = xhr =>
|
||||||
{
|
{
|
||||||
const token: string = this.oidcSecurity.getAccessToken();
|
const token: string = null;
|
||||||
if (token)
|
if (token)
|
||||||
xhr.setRequestHeader("Authorization", "Bearer " + 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
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
namespace Kyoo.Authentication.Models.DTO
|
using System;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Permissions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A model to represent an otac request
|
/// The annotated route can only be accessed by a logged in user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class OtacRequest
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||||
|
public class UserOnlyAttribute : Attribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
// TODO: Implement a Filter Attribute to make this work. For now, this attribute is only useful as documentation.
|
||||||
/// The One Time Access Code
|
|
||||||
/// </summary>
|
|
||||||
public string Otac { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Should the user stay logged
|
|
||||||
/// </summary>
|
|
||||||
public bool StayLoggedIn { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -17,6 +17,7 @@
|
|||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
namespace Kyoo.Abstractions.Models
|
namespace Kyoo.Abstractions.Models
|
||||||
{
|
{
|
||||||
@ -44,6 +45,7 @@ namespace Kyoo.Abstractions.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The user password (hashed, it can't be read like that). The hashing format is implementation defined.
|
/// The user password (hashed, it can't be read like that). The hashing format is implementation defined.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[SerializeIgnore]
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -54,6 +56,7 @@ namespace Kyoo.Abstractions.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Arbitrary extra data that can be used by specific authentication implementations.
|
/// Arbitrary extra data that can be used by specific authentication implementations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[SerializeIgnore]
|
||||||
public Dictionary<string, string> ExtraData { get; set; }
|
public Dictionary<string, string> ExtraData { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -62,11 +65,13 @@ namespace Kyoo.Abstractions.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of shows the user has finished.
|
/// The list of shows the user has finished.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[SerializeIgnore]
|
||||||
public ICollection<Show> Watched { get; set; }
|
public ICollection<Show> Watched { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of episodes the user is watching (stopped in progress or the next episode of the show)
|
/// The list of episodes the user is watching (stopped in progress or the next episode of the show)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[SerializeIgnore]
|
||||||
public ICollection<WatchedEpisode> CurrentlyWatching { get; set; }
|
public ICollection<WatchedEpisode> CurrentlyWatching { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,25 +31,30 @@ namespace Kyoo.Abstractions.Models.Utils
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const int AlternativeRoute = 1;
|
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>
|
/// <summary>
|
||||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for main resources of kyoo.
|
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for main resources of kyoo.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string ResourcesGroup = "0:Resources";
|
public const string ResourcesGroup = "1:Resources";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A group name for <see cref="ApiDefinitionAttribute"/>.
|
/// A group name for <see cref="ApiDefinitionAttribute"/>.
|
||||||
/// It should be used for sub resources of kyoo that help define the main resources.
|
/// It should be used for sub resources of kyoo that help define the main resources.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string MetadataGroup = "1:Metadata";
|
public const string MetadataGroup = "2:Metadata";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints useful for playback.
|
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints useful for playback.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string WatchGroup = "2:Watch";
|
public const string WatchGroup = "3:Watch";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by admins.
|
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by admins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string AdminGroup = "3:Admin";
|
public const string AdminGroup = "4:Admin";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,28 +18,16 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Text;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using Autofac;
|
using Autofac;
|
||||||
using IdentityServer4.Extensions;
|
|
||||||
using IdentityServer4.Models;
|
|
||||||
using IdentityServer4.Services;
|
|
||||||
using Kyoo.Abstractions;
|
using Kyoo.Abstractions;
|
||||||
using Kyoo.Abstractions.Controllers;
|
using Kyoo.Abstractions.Controllers;
|
||||||
using Kyoo.Authentication.Models;
|
using Kyoo.Authentication.Models;
|
||||||
using Kyoo.Authentication.Views;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.IdentityModel.Logging;
|
|
||||||
using SameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode;
|
|
||||||
|
|
||||||
namespace Kyoo.Authentication
|
namespace Kyoo.Authentication
|
||||||
{
|
{
|
||||||
@ -55,14 +43,13 @@ namespace Kyoo.Authentication
|
|||||||
public string Name => "Authentication";
|
public string Name => "Authentication";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Description => "Enable OpenID authentication for Kyoo.";
|
public string Description => "Enable an authentication/permission system for Kyoo (via Jwt or ApiKeys).";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Dictionary<string, Type> Configuration => new()
|
public Dictionary<string, Type> Configuration => new()
|
||||||
{
|
{
|
||||||
{ AuthenticationOption.Path, typeof(AuthenticationOption) },
|
{ AuthenticationOption.Path, typeof(AuthenticationOption) },
|
||||||
{ PermissionOption.Path, typeof(PermissionOption) },
|
{ PermissionOption.Path, typeof(PermissionOption) },
|
||||||
{ CertificateOption.Path, typeof(CertificateOption) }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -71,130 +58,50 @@ namespace Kyoo.Authentication
|
|||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The logger used to allow IdentityServer to log things.
|
/// Create a new authentication module instance and use the given configuration.
|
||||||
/// </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.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configuration">The configuration to use</param>
|
/// <param name="configuration">The configuration to use</param>
|
||||||
/// <param name="logger">The logger used to allow IdentityServer to log things</param>
|
public AuthenticationModule(IConfiguration configuration)
|
||||||
/// <param name="environment">The environment information to check if the app runs in debug mode</param>
|
|
||||||
[SuppressMessage("ReSharper", "ContextualLoggerProblem",
|
|
||||||
Justification = "The logger is used for a dependency that is not created via the container.")]
|
|
||||||
public AuthenticationModule(IConfiguration configuration,
|
|
||||||
ILogger<DefaultCorsPolicyService> logger,
|
|
||||||
IWebHostEnvironment environment)
|
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_logger = logger;
|
|
||||||
_environment = environment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Configure(ContainerBuilder builder)
|
public void Configure(ContainerBuilder builder)
|
||||||
{
|
{
|
||||||
builder.RegisterType<PermissionValidator>().As<IPermissionValidator>().SingleInstance();
|
builder.RegisterType<PermissionValidator>().As<IPermissionValidator>().SingleInstance();
|
||||||
|
builder.RegisterType<TokenController>().As<ITokenController>().SingleInstance();
|
||||||
DefaultCorsPolicyService cors = new(_logger)
|
|
||||||
{
|
|
||||||
AllowedOrigins = { _configuration.GetPublicUrl().GetLeftPart(UriPartial.Authority) }
|
|
||||||
};
|
|
||||||
builder.RegisterInstance(cors).As<ICorsPolicyService>().SingleInstance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Configure(IServiceCollection services)
|
public void Configure(IServiceCollection services)
|
||||||
{
|
{
|
||||||
Uri publicUrl = _configuration.GetPublicUrl();
|
Uri publicUrl = _configuration.GetPublicUrl();
|
||||||
|
AuthenticationOption jwt = ConfigurationBinder.Get<AuthenticationOption>(
|
||||||
if (_environment.IsDevelopment())
|
_configuration.GetSection(AuthenticationOption.Path)
|
||||||
IdentityModelEventSource.ShowPII = true;
|
);
|
||||||
|
|
||||||
services.AddControllers();
|
|
||||||
|
|
||||||
// TODO handle direct-videos with bearers (probably add a cookie and a app.Use to translate that for videos)
|
// 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.
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
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()
|
|
||||||
.AddJwtBearer(options =>
|
.AddJwtBearer(options =>
|
||||||
{
|
{
|
||||||
options.Authority = publicUrl.ToString();
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
options.Audience = "kyoo";
|
{
|
||||||
options.RequireHttpsMetadata = false;
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = publicUrl.ToString(),
|
||||||
|
ValidAudience = publicUrl.ToString(),
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwt.Secret))
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<IStartupAction> ConfigureSteps => new IStartupAction[]
|
public IEnumerable<IStartupAction> ConfigureSteps => new IStartupAction[]
|
||||||
{
|
{
|
||||||
SA.New<IApplicationBuilder>(app =>
|
SA.New<IApplicationBuilder>(app => app.UseAuthentication(), SA.Authentication),
|
||||||
{
|
|
||||||
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)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
/// <summary>
|
||||||
/// The permission to validate.
|
/// The permission to validate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly string _permission;
|
private readonly string? _permission;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The kind of permission needed.
|
/// The kind of permission needed.
|
||||||
@ -96,7 +96,10 @@ namespace Kyoo.Authentication
|
|||||||
/// <param name="kind">The kind of permission needed.</param>
|
/// <param name="kind">The kind of permission needed.</param>
|
||||||
/// <param name="group">The group of the permission.</param>
|
/// <param name="group">The group of the permission.</param>
|
||||||
/// <param name="options">The option containing default values.</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)
|
IOptionsMonitor<PermissionOption> options)
|
||||||
{
|
{
|
||||||
_permission = permission;
|
_permission = permission;
|
||||||
@ -133,7 +136,7 @@ namespace Kyoo.Authentication
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||||
{
|
{
|
||||||
string permission = _permission;
|
string? permission = _permission;
|
||||||
Kind? kind = _kind;
|
Kind? kind = _kind;
|
||||||
|
|
||||||
if (permission == null || kind == null)
|
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
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using IdentityModel;
|
|
||||||
using IdentityServer4;
|
|
||||||
using Kyoo.Abstractions.Models;
|
|
||||||
|
|
||||||
namespace Kyoo.Authentication
|
namespace Kyoo.Authentication
|
||||||
{
|
{
|
||||||
@ -30,35 +28,6 @@ namespace Kyoo.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Extensions
|
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>
|
/// <summary>
|
||||||
/// Get the permissions of an user.
|
/// Get the permissions of an user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -66,7 +35,8 @@ namespace Kyoo.Authentication
|
|||||||
/// <returns>The list of permissions</returns>
|
/// <returns>The list of permissions</returns>
|
||||||
public static ICollection<string> GetPermissions(this ClaimsPrincipal user)
|
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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,16 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<LoginRoot>../Kyoo.WebLogin/</LoginRoot>
|
<LoginRoot>../Kyoo.WebLogin/</LoginRoot>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="IdentityServer4" Version="4.1.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.12" />
|
||||||
<PackageReference Include="IdentityServer4.Storage" Version="4.1.2" />
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
|
|
||||||
<PackageReference Include="Portable.BouncyCastle" Version="1.8.10" />
|
|
||||||
|
|
||||||
<ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" />
|
<ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Remove="$(LoginRoot)**;" />
|
|
||||||
<Content Include="$(LoginRoot)**" Visible="false">
|
|
||||||
<Link>login/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
</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; }
|
public string Password { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public bool StayLoggedIn { get; set; }
|
/// <param name="username">The user's username.</param>
|
||||||
|
/// <param name="password">The user's password.</param>
|
||||||
/// <summary>
|
public LoginRequest(string username, string password)
|
||||||
/// The return url of the login flow.
|
{
|
||||||
/// </summary>
|
Username = username;
|
||||||
public string ReturnURL { get; set; }
|
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 System.ComponentModel.DataAnnotations;
|
||||||
using Kyoo.Abstractions.Models;
|
using Kyoo.Abstractions.Models;
|
||||||
using Kyoo.Utils;
|
using Kyoo.Utils;
|
||||||
|
using BCryptNet = BCrypt.Net.BCrypt;
|
||||||
|
|
||||||
namespace Kyoo.Authentication.Models.DTO
|
namespace Kyoo.Authentication.Models.DTO
|
||||||
{
|
{
|
||||||
@ -43,9 +44,22 @@ namespace Kyoo.Authentication.Models.DTO
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The user's password.
|
/// The user's password.
|
||||||
/// </summary>
|
/// </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; }
|
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>
|
/// <summary>
|
||||||
/// Convert this register request to a new <see cref="User"/> class.
|
/// Convert this register request to a new <see cref="User"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -56,7 +70,7 @@ namespace Kyoo.Authentication.Models.DTO
|
|||||||
{
|
{
|
||||||
Slug = Utility.ToSlug(Username),
|
Slug = Utility.ToSlug(Username),
|
||||||
Username = Username,
|
Username = Username,
|
||||||
Password = Password,
|
Password = BCryptNet.HashPassword(Password),
|
||||||
Email = Email,
|
Email = Email,
|
||||||
ExtraData = new Dictionary<string, string>()
|
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";
|
public const string Path = "authentication";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The options for certificates
|
/// The default jwt secret.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Options for permissions
|
/// Options for permissions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PermissionOption Permissions { get; set; }
|
public PermissionOption Permissions { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Root path of user's profile pictures.
|
/// Root path of user's profile pictures.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// The default permissions that will be given to a non-connected user.
|
/// The default permissions that will be given to a non-connected user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string[] Default { get; set; }
|
public string[] Default { get; set; } = new[] { "overall.read", "overall.write" };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Permissions applied to a new user.
|
/// Permissions applied to a new user.
|
||||||
/// </summary>
|
/// </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 />
|
/// <inheritdoc />
|
||||||
public void Configure(IServiceCollection services)
|
public void Configure(IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
services.AddHttpContextAccessor();
|
||||||
services.AddTransient<IConfigureOptions<MvcNewtonsoftJsonOptions>, JsonOptions>();
|
services.AddTransient<IConfigureOptions<MvcNewtonsoftJsonOptions>, JsonOptions>();
|
||||||
|
|
||||||
services.AddMvcCore()
|
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>
|
/// <response code="404">No item could be found with the given id or slug.</response>
|
||||||
[HttpDelete("{identifier:id}")]
|
[HttpDelete("{identifier:id}")]
|
||||||
[PartialPermission(Kind.Delete)]
|
[PartialPermission(Kind.Delete)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> Delete(Identifier identifier)
|
public async Task<IActionResult> Delete(Identifier identifier)
|
||||||
{
|
{
|
||||||
@ -236,7 +236,7 @@ namespace Kyoo.Core.Api
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -250,7 +250,7 @@ namespace Kyoo.Core.Api
|
|||||||
/// <response code="400">One or multiple filters are invalid.</response>
|
/// <response code="400">One or multiple filters are invalid.</response>
|
||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
[PartialPermission(Kind.Delete)]
|
[PartialPermission(Kind.Delete)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
||||||
public async Task<IActionResult> Delete([FromQuery] Dictionary<string, string> where)
|
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 BadRequest(new RequestError(ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok();
|
return NoContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ namespace Kyoo.Core.Api
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("subtitles")]
|
[Route("subtitles")]
|
||||||
[Route("subtitle", Order = AlternativeRoute)]
|
[Route("subtitle", Order = AlternativeRoute)]
|
||||||
[PartialPermission(nameof(SubtitleApi))]
|
[PartialPermission("subtitle")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[ApiDefinition("Subtitles", Group = WatchGroup)]
|
[ApiDefinition("Subtitles", Group = WatchGroup)]
|
||||||
public class SubtitleApi : ControllerBase
|
public class SubtitleApi : ControllerBase
|
||||||
|
@ -161,7 +161,8 @@ namespace Kyoo.Host.Generic
|
|||||||
.OrderByDescending(x => x.Priority);
|
.OrderByDescending(x => x.Priority);
|
||||||
|
|
||||||
using ILifetimeScope scope = container.BeginLifetimeScope(x =>
|
using ILifetimeScope scope = container.BeginLifetimeScope(x =>
|
||||||
x.RegisterInstance(app).SingleInstance().ExternallyOwned());
|
x.RegisterInstance(app).SingleInstance().ExternallyOwned()
|
||||||
|
);
|
||||||
IServiceProvider provider = scope.Resolve<IServiceProvider>();
|
IServiceProvider provider = scope.Resolve<IServiceProvider>();
|
||||||
foreach (IStartupAction step in steps)
|
foreach (IStartupAction step in steps)
|
||||||
step.Run(provider);
|
step.Run(provider);
|
||||||
|
@ -60,17 +60,12 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"authentication": {
|
"authentication": {
|
||||||
"certificate": {
|
|
||||||
"file": "certificate.pfx",
|
|
||||||
"oldFile": "oldCertificate.pfx",
|
|
||||||
"password": "passphrase"
|
|
||||||
},
|
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"default": ["overall.read", "overall.write", "overall.create", "overall.delete", "admin.read", "admin.write"],
|
"default": ["overall.read", "overall.write"],
|
||||||
"newUser": ["overall.read", "overall.write", "overall.create", "overall.delete", "admin.read", "admin.write"]
|
"newUser": ["overall.read", "overall.write"]
|
||||||
},
|
},
|
||||||
"profilePicturePath": "users/",
|
"profilePicturePath": "users/",
|
||||||
"clients": []
|
"secret": "4c@mraGB!KRfF@kpS8740y9FcHemKxBsqqxLbdR?"
|
||||||
},
|
},
|
||||||
|
|
||||||
"tvdb": {
|
"tvdb": {
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@ -36,12 +37,19 @@ namespace Kyoo.Swagger
|
|||||||
public bool Process(OperationProcessorContext context)
|
public bool Process(OperationProcessorContext context)
|
||||||
{
|
{
|
||||||
context.OperationDescription.Operation.Security ??= new List<OpenApiSecurityRequirement>();
|
context.OperationDescription.Operation.Security ??= new List<OpenApiSecurityRequirement>();
|
||||||
OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes<PermissionAttribute>()
|
OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes<UserOnlyAttribute>()
|
||||||
.Aggregate(new OpenApiSecurityRequirement(), (agg, cur) =>
|
.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);
|
ICollection<string> permissions = _GetPermissionsList(agg, cur.Group);
|
||||||
permissions.Add($"{cur.Type}.{cur.Kind.ToString().ToLower()}");
|
permissions.Add($"{cur.Type}.{cur.Kind.ToString().ToLower()}");
|
||||||
agg[cur.Group.ToString()] = permissions;
|
agg[nameof(Kyoo)] = permissions;
|
||||||
return agg;
|
return agg;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -61,7 +69,7 @@ namespace Kyoo.Swagger
|
|||||||
: cur.Kind;
|
: cur.Kind;
|
||||||
ICollection<string> permissions = _GetPermissionsList(agg, group);
|
ICollection<string> permissions = _GetPermissionsList(agg, group);
|
||||||
permissions.Add($"{type}.{kind.ToString().ToLower()}");
|
permissions.Add($"{type}.{kind.ToString().ToLower()}");
|
||||||
agg[group.ToString()] = permissions;
|
agg[nameof(Kyoo)] = permissions;
|
||||||
return agg;
|
return agg;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -70,7 +78,7 @@ namespace Kyoo.Swagger
|
|||||||
return true;
|
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)
|
return security.TryGetValue(group.ToString(), out IEnumerable<string> perms)
|
||||||
? perms.ToList()
|
? perms.ToList()
|
||||||
|
@ -120,35 +120,14 @@ namespace Kyoo.Swagger
|
|||||||
}));
|
}));
|
||||||
document.SchemaProcessors.Add(new ThumbnailProcessor());
|
document.SchemaProcessors.Add(new ThumbnailProcessor());
|
||||||
|
|
||||||
document.AddSecurity("Kyoo", new OpenApiSecurityScheme
|
document.AddSecurity(nameof(Kyoo), new OpenApiSecurityScheme
|
||||||
{
|
{
|
||||||
Type = OpenApiSecuritySchemeType.OpenIdConnect,
|
Type = OpenApiSecuritySchemeType.Http,
|
||||||
OpenIdConnectUrl = "/.well-known/openid-configuration",
|
Scheme = "Bearer",
|
||||||
Description = "You can login via an OIDC client, clients must be first registered in kyoo. " +
|
BearerFormat = "JWT",
|
||||||
"Documentation coming soon."
|
Description = "The user's bearer"
|
||||||
});
|
});
|
||||||
document.OperationProcessors.Add(new OperationPermissionProcessor());
|
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