mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-08-23 14:35:24 -07:00
Added Authentication with OmbiUserbase using Cloudflare Access JWT
This commit is contained in:
parent
e527a7e77d
commit
88adf280bd
15 changed files with 335 additions and 57 deletions
|
@ -52,7 +52,7 @@ namespace Ombi.Core.Authentication
|
||||||
IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<OmbiUser>> logger, IPlexApi plexApi,
|
IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<OmbiUser>> logger, IPlexApi plexApi,
|
||||||
IEmbyApiFactory embyApi, ISettingsService<EmbySettings> embySettings,
|
IEmbyApiFactory embyApi, ISettingsService<EmbySettings> embySettings,
|
||||||
IJellyfinApiFactory jellyfinApi, ISettingsService<JellyfinSettings> jellyfinSettings,
|
IJellyfinApiFactory jellyfinApi, ISettingsService<JellyfinSettings> jellyfinSettings,
|
||||||
ISettingsService<AuthenticationSettings> auth)
|
ISettingsService<AuthenticationSettings> auth, ISettingsService<CloudflareAuthenticationSettings> cfauth)
|
||||||
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
|
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
|
||||||
{
|
{
|
||||||
_plexApi = plexApi;
|
_plexApi = plexApi;
|
||||||
|
|
|
@ -13,5 +13,6 @@ namespace Ombi.Settings.Settings.Models
|
||||||
public bool RequireNonAlphanumeric { get; set; }
|
public bool RequireNonAlphanumeric { get; set; }
|
||||||
public bool RequireUppercase { get; set; }
|
public bool RequireUppercase { get; set; }
|
||||||
public bool EnableOAuth { get; set; } // Plex OAuth
|
public bool EnableOAuth { get; set; } // Plex OAuth
|
||||||
|
public bool EnableCloudflareAccess { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
11
src/Ombi.Settings/Settings/Models/CloudflareSettings.cs
Normal file
11
src/Ombi.Settings/Settings/Models/CloudflareSettings.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ombi.Settings.Settings.Models
|
||||||
|
{
|
||||||
|
public class CloudflareAuthenticationSettings : Settings
|
||||||
|
{
|
||||||
|
public string issuer { get; set; }
|
||||||
|
public string audience { get; set; }
|
||||||
|
public string certlink { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,10 @@ export class AuthService extends ServiceHelpers {
|
||||||
return this.http.post<boolean>(`${this.url}/requirePassword`, JSON.stringify(login), { headers: this.headers });
|
return this.http.post<boolean>(`${this.url}/requirePassword`, JSON.stringify(login), { headers: this.headers });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public attemptCF(): Observable<any> {
|
||||||
|
return this.http.get<any>(`${this.url}/cfAuth`);
|
||||||
|
}
|
||||||
|
|
||||||
public getToken() {
|
public getToken() {
|
||||||
return this.jwtHelperService.tokenGetter();
|
return this.jwtHelperService.tokenGetter();
|
||||||
}
|
}
|
||||||
|
|
|
@ -230,6 +230,13 @@ export interface IAuthenticationSettings extends ISettings {
|
||||||
requireNonAlphanumeric: boolean;
|
requireNonAlphanumeric: boolean;
|
||||||
requireUppercase: boolean;
|
requireUppercase: boolean;
|
||||||
enableOAuth: boolean;
|
enableOAuth: boolean;
|
||||||
|
enableCloudflareAccess: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICloudflareSettings extends ISettings {
|
||||||
|
issuer: string;
|
||||||
|
audience: string;
|
||||||
|
certlink: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICustomPage extends ISettings {
|
export interface ICustomPage extends ISettings {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<H1 *ngIf="!customizationSettings.logo && !customizationSettings.applicationName" class="login_logo">OMBI</H1>
|
<H1 *ngIf="!customizationSettings.logo && !customizationSettings.applicationName" class="login_logo">OMBI</H1>
|
||||||
<H1 *ngIf="customizationSettings.applicationName && !customizationSettings.logo" [ngClass]="{'bigText': customizationSettings.applicationName.length >= 7 && customizationSettings.applicationName.length < 14, 'hugeText': customizationSettings.applicationName.length >= 14 }" class="login_logo custom">{{customizationSettings.applicationName}}</H1>
|
<H1 *ngIf="customizationSettings.applicationName && !customizationSettings.logo" [ngClass]="{'bigText': customizationSettings.applicationName.length >= 7 && customizationSettings.applicationName.length < 14, 'hugeText': customizationSettings.applicationName.length >= 14 }" class="login_logo custom">{{customizationSettings.applicationName}}</H1>
|
||||||
<img mat-card-image *ngIf="customizationSettings.logo" [src]="customizationSettings.logo" class="logo-img">
|
<img mat-card-image *ngIf="customizationSettings.logo" [src]="customizationSettings.logo" class="logo-img">
|
||||||
<mat-card-content id="login-box" *ngIf="!authenticationSettings.enableOAuth || loginWithOmbi">
|
<mat-card-content id="login-box" *ngIf="!(authenticationSettings.enableOAuth||authenticationSettings.enableCloudflareAccess) || loginWithOmbi">
|
||||||
<form *ngIf="authenticationSettings" class="form-signin" novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
|
<form *ngIf="authenticationSettings" class="form-signin" novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,11 +35,15 @@
|
||||||
</form>
|
</form>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
|
|
||||||
<mat-card-content *ngIf="authenticationSettings.enableOAuth && !loginWithOmbi" class="login-buttons">
|
<mat-card-content *ngIf="(!loginWithOmbi && (authenticationSettings.enableOAuth||authenticationSettings.enableCloudflareAccess))" class="login-buttons">
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<button id="sign-in" mat-raised-button color="primary" type="submit" data-cy="OmbiButton" (click)="loginWithOmbi = true">{{'Login.SignInWith' | translate:appNameTranslate}}</button>
|
<button id="sign-in" mat-raised-button color="primary" type="submit" data-cy="OmbiButton" (click)="loginWithOmbi = true">{{'Login.SignInWith' | translate:appNameTranslate}}</button>
|
||||||
<button id="sign-plex" mat-raised-button color="accent" type="button" data-cy="oAuthPlexButton" (click)="oauth()">
|
<button id="sign-cf" *ngIf="authenticationSettings.enableCloudflareAccess" mat-raised-button color="primary" type="button" data-cy="CfButton" (click)="cfauth()">
|
||||||
|
<span *ngIf="!cfLoading">{{'Login.SignInWithCF' | translate}}</span>
|
||||||
|
<span *ngIf="cfLoading"><i class="fas fa-circle-notch fa-spin fa-fw"></i></span>
|
||||||
|
</button>
|
||||||
|
<button id="sign-plex" *ngIf="authenticationSettings.enableOAuth" mat-raised-button color="accent" type="button" data-cy="oAuthPlexButton" (click)="oauth()">
|
||||||
<span *ngIf="!oauthLoading">{{'Login.SignInWithPlex' | translate}}</span>
|
<span *ngIf="!oauthLoading">{{'Login.SignInWithPlex' | translate}}</span>
|
||||||
<span *ngIf="oauthLoading"><i class="fas fa-circle-notch fa-spin fa-fw"></i></span>
|
<span *ngIf="oauthLoading"><i class="fas fa-circle-notch fa-spin fa-fw"></i></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -34,6 +34,7 @@ export class LoginComponent implements OnDestroy, OnInit {
|
||||||
public loginWithOmbi: boolean;
|
public loginWithOmbi: boolean;
|
||||||
public pinTimer: any;
|
public pinTimer: any;
|
||||||
public oauthLoading: boolean;
|
public oauthLoading: boolean;
|
||||||
|
public cfLoading: boolean;
|
||||||
|
|
||||||
public get appName(): string {
|
public get appName(): string {
|
||||||
if (this.customizationSettings.applicationName) {
|
if (this.customizationSettings.applicationName) {
|
||||||
|
@ -101,7 +102,6 @@ export class LoginComponent implements OnDestroy, OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
|
|
||||||
this.customziationFacade.settings$().subscribe(x => this.customizationSettings = x);
|
this.customziationFacade.settings$().subscribe(x => this.customizationSettings = x);
|
||||||
|
|
||||||
this.settingsService
|
this.settingsService
|
||||||
|
@ -173,6 +173,34 @@ export class LoginComponent implements OnDestroy, OnInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public cfauth() {
|
||||||
|
this.cfLoading = true;
|
||||||
|
this.translate
|
||||||
|
.get("Login.Errors.UnauthorizedCFAccount")
|
||||||
|
.subscribe((x) => (this.errorBody = x));
|
||||||
|
|
||||||
|
this.authService.attemptCF().subscribe(
|
||||||
|
(x) => {
|
||||||
|
this.store.save("id_token", x.access_token);
|
||||||
|
|
||||||
|
if (this.authService.loggedIn()) {
|
||||||
|
this.ngOnDestroy();
|
||||||
|
this.router.navigate(["/"]);
|
||||||
|
} else {
|
||||||
|
this.notify.open(this.errorBody, "OK", {
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this.notify.open(this.errorBody, "OK", {
|
||||||
|
duration: 3000000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.cfLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
public oauth() {
|
public oauth() {
|
||||||
if (this.oAuthWindow) {
|
if (this.oAuthWindow) {
|
||||||
this.oAuthWindow.close();
|
this.oAuthWindow.close();
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { Observable } from "rxjs";
|
||||||
import {
|
import {
|
||||||
IAbout,
|
IAbout,
|
||||||
IAuthenticationSettings,
|
IAuthenticationSettings,
|
||||||
|
ICloudflareSettings,
|
||||||
ICouchPotatoSettings,
|
ICouchPotatoSettings,
|
||||||
ICronTestModel,
|
ICronTestModel,
|
||||||
ICronViewModelBody,
|
ICronViewModelBody,
|
||||||
|
@ -125,6 +126,10 @@ export class SettingsService extends ServiceHelpers {
|
||||||
return this.http.get<IAuthenticationSettings>(`${this.url}/Authentication`, {headers: this.headers});
|
return this.http.get<IAuthenticationSettings>(`${this.url}/Authentication`, {headers: this.headers});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getCloudflareAuthentication(): Observable<ICloudflareSettings> {
|
||||||
|
return this.http.get<ICloudflareSettings>(`${this.url}/Cloudflare`, {headers: this.headers});
|
||||||
|
}
|
||||||
|
|
||||||
public getClientId(): Observable<string> {
|
public getClientId(): Observable<string> {
|
||||||
return this.http.get<string>(`${this.url}/clientid`, {headers: this.headers});
|
return this.http.get<string>(`${this.url}/clientid`, {headers: this.headers});
|
||||||
}
|
}
|
||||||
|
@ -133,6 +138,10 @@ export class SettingsService extends ServiceHelpers {
|
||||||
return this.http.post<boolean>(`${this.url}/Authentication`, JSON.stringify(settings), {headers: this.headers});
|
return this.http.post<boolean>(`${this.url}/Authentication`, JSON.stringify(settings), {headers: this.headers});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public saveCloudflareAuthentication(settings: ICloudflareSettings): Observable<boolean> {
|
||||||
|
return this.http.post<boolean>(`${this.url}/Cloudflare`, JSON.stringify(settings), {headers: this.headers});
|
||||||
|
}
|
||||||
|
|
||||||
// Using http since we need it not to be authenticated to get the landing page settings
|
// Using http since we need it not to be authenticated to get the landing page settings
|
||||||
public getLandingPage(): Observable<ILandingPageSettings> {
|
public getLandingPage(): Observable<ILandingPageSettings> {
|
||||||
return this.http.get<ILandingPageSettings>(`${this.url}/LandingPage`, {headers: this.headers});
|
return this.http.get<ILandingPageSettings>(`${this.url}/LandingPage`, {headers: this.headers});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<settings-menu></settings-menu>
|
<settings-menu></settings-menu>
|
||||||
<div class="small-middle-container">
|
<div class="small-middle-container">
|
||||||
<wiki></wiki>
|
<wiki></wiki>
|
||||||
<fieldset *ngIf="form">
|
<fieldset *ngIf="form">
|
||||||
<legend>Authentication</legend>
|
<legend>Authentication</legend>
|
||||||
<div class="md-form-field" style="margin-top:1em;"></div>
|
<div class="md-form-field" style="margin-top:1em;"></div>
|
||||||
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
|
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
|
||||||
|
@ -18,6 +18,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="checkbox">
|
||||||
|
<mat-slide-toggle id="enableCloudflareAccess" name="enableCloudflareAccess" formControlName="enableCloudflareAccess">Enable Cloudflare Access Auth</mat-slide-toggle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- <hr/>
|
<!-- <hr/>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
|
@ -62,5 +69,43 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<fieldset *ngIf="cfform">
|
||||||
|
<hr>
|
||||||
|
<br>
|
||||||
|
<div>
|
||||||
|
<legend>Cloudflare Authentication</legend>
|
||||||
|
<div class="md-form-field" style="margin-top:1em;"></div>
|
||||||
|
<form novalidate [formGroup]="cfform" (ngSubmit)="oncfSubmit(cfform)">
|
||||||
|
<div class="form-group">
|
||||||
|
<mat-form-field appearance="fill">
|
||||||
|
<mat-label>Audience TAG</mat-label>
|
||||||
|
<input matInput id="audience" name="audience" formControlName="audience">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<mat-form-field appearance="fill">
|
||||||
|
<mat-label>JWT Certificate URL</mat-label>
|
||||||
|
<input matInput id="certlink" name="certlink" formControlName="certlink">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<mat-form-field appearance="fill">
|
||||||
|
<mat-label>Issuer URL</mat-label>
|
||||||
|
<input matInput id="issuer" name="issuer" formControlName="issuer">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div>
|
||||||
|
<button mat-raised-button type="submit" color="primary" [disabled]="form.invalid" class="mat-focus-indicator mat-stroked-button accent mat-accent mat-raised-button mat-button-base" ng-reflect-disabled="false">
|
||||||
|
<span class="mat-button-wrapper">Submit</span><div matripple="" class="mat-ripple mat-button-ripple" ng-reflect-disabled="false" ng-reflect-centered="false" ng-reflect-trigger="[object HTMLButtonElement]"></div>
|
||||||
|
<div class="mat-button-focus-overlay"></div></button><div class="md-form-field" style="margin-top:1em;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
|
@ -11,6 +11,7 @@ import { SettingsService } from "../../services";
|
||||||
export class AuthenticationComponent implements OnInit {
|
export class AuthenticationComponent implements OnInit {
|
||||||
|
|
||||||
public form: FormGroup;
|
public form: FormGroup;
|
||||||
|
public cfform: FormGroup;
|
||||||
|
|
||||||
constructor(private settingsService: SettingsService,
|
constructor(private settingsService: SettingsService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
|
@ -26,6 +27,14 @@ export class AuthenticationComponent implements OnInit {
|
||||||
requireNonAlphanumeric: [x.requireNonAlphanumeric],
|
requireNonAlphanumeric: [x.requireNonAlphanumeric],
|
||||||
requireUppercase: [x.requireUppercase],
|
requireUppercase: [x.requireUppercase],
|
||||||
enableOAuth: [x.enableOAuth],
|
enableOAuth: [x.enableOAuth],
|
||||||
|
enableCloudflareAccess: [x.enableCloudflareAccess],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.settingsService.getCloudflareAuthentication().subscribe(x => {
|
||||||
|
this.cfform = this.fb.group({
|
||||||
|
audience: [x.audience],
|
||||||
|
certlink: [x.certlink],
|
||||||
|
issuer: [x.issuer],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -44,4 +53,19 @@ export class AuthenticationComponent implements OnInit {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public oncfSubmit(form: FormGroup) {
|
||||||
|
if (form.invalid) {
|
||||||
|
this.notificationService.error("Please check your entered values");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.settingsService.saveCloudflareAuthentication(form.value).subscribe(x => {
|
||||||
|
if (x) {
|
||||||
|
this.notificationService.success("Successfully saved Cloudflare Authentication settings");
|
||||||
|
} else {
|
||||||
|
this.notificationService.success("There was an error when saving the Cloudflare Authentication settings");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,10 @@ td.mat-cell {
|
||||||
background-color: $ombi-active;
|
background-color: $ombi-active;
|
||||||
color: $ombi-active-text;
|
color: $ombi-active-text;
|
||||||
}
|
}
|
||||||
|
&#sign-cf{
|
||||||
|
background-color: #282A2D;
|
||||||
|
color: #E5A00D;
|
||||||
|
}
|
||||||
&#sign-plex{
|
&#sign-plex{
|
||||||
background-color: #282A2D;
|
background-color: #282A2D;
|
||||||
color: #E5A00D;
|
color: #E5A00D;
|
||||||
|
|
|
@ -467,6 +467,39 @@ namespace Ombi.Controllers.V1
|
||||||
return await Get<AuthenticationSettings>();
|
return await Get<AuthenticationSettings>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Cloudflare Authentication Settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("cloudflare")]
|
||||||
|
public async Task<CloudflareAuthenticationSettings> CloudflareAuthenticationsSettings()
|
||||||
|
{
|
||||||
|
return await Get<CloudflareAuthenticationSettings>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save the Cloudflare Authentication Settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("cloudflare")]
|
||||||
|
public async Task<bool> CloudflareAuthenticationsSettings([FromBody]CloudflareAuthenticationSettings settings)
|
||||||
|
{
|
||||||
|
if (settings.audience.IsNullOrEmpty() && settings.certlink.IsNullOrEmpty() && settings.issuer.IsNullOrEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ((settings.audience.IsNullOrEmpty() || settings.certlink.IsNullOrEmpty() || settings.issuer.IsNullOrEmpty())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Uri uriResult;
|
||||||
|
if ((!Uri.TryCreate(settings.certlink, UriKind.Absolute, out uriResult)) || !(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps)
|
||||||
|
|| (!Uri.TryCreate(settings.issuer, UriKind.Absolute, out uriResult)) || !(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
settings.certlink = settings.certlink.TrimEnd('/');
|
||||||
|
settings.issuer = settings.issuer.TrimEnd('/');
|
||||||
|
return await Save(settings);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save the Radarr settings.
|
/// Save the Radarr settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -2,18 +2,23 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Ombi.Core.Authentication;
|
using Ombi.Core.Authentication;
|
||||||
|
using Ombi.Core.Settings;
|
||||||
using Ombi.Helpers;
|
using Ombi.Helpers;
|
||||||
using Ombi.Models;
|
using Ombi.Models;
|
||||||
using Ombi.Models.External;
|
using Ombi.Models.External;
|
||||||
using Ombi.Models.Identity;
|
using Ombi.Models.Identity;
|
||||||
|
using Ombi.Settings.Settings.Models;
|
||||||
using Ombi.Store.Entities;
|
using Ombi.Store.Entities;
|
||||||
using Ombi.Store.Repository;
|
using Ombi.Store.Repository;
|
||||||
|
|
||||||
|
@ -25,13 +30,16 @@ namespace Ombi.Controllers.V1
|
||||||
public class TokenController : ControllerBase
|
public class TokenController : ControllerBase
|
||||||
{
|
{
|
||||||
public TokenController(OmbiUserManager um, IOptions<TokenAuthentication> ta, ITokenRepository token,
|
public TokenController(OmbiUserManager um, IOptions<TokenAuthentication> ta, ITokenRepository token,
|
||||||
IPlexOAuthManager oAuthManager, ILogger<TokenController> logger)
|
IPlexOAuthManager oAuthManager, ILogger<TokenController> logger,
|
||||||
|
ISettingsService<AuthenticationSettings> auth, ISettingsService<CloudflareAuthenticationSettings> cfauth)
|
||||||
{
|
{
|
||||||
_userManager = um;
|
_userManager = um;
|
||||||
_tokenAuthenticationOptions = ta.Value;
|
_tokenAuthenticationOptions = ta.Value;
|
||||||
_token = token;
|
_token = token;
|
||||||
_plexOAuthManager = oAuthManager;
|
_plexOAuthManager = oAuthManager;
|
||||||
_log = logger;
|
_log = logger;
|
||||||
|
_authSettings = auth;
|
||||||
|
_cfsettings = cfauth;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly TokenAuthentication _tokenAuthenticationOptions;
|
private readonly TokenAuthentication _tokenAuthenticationOptions;
|
||||||
|
@ -39,6 +47,10 @@ namespace Ombi.Controllers.V1
|
||||||
private readonly OmbiUserManager _userManager;
|
private readonly OmbiUserManager _userManager;
|
||||||
private readonly IPlexOAuthManager _plexOAuthManager;
|
private readonly IPlexOAuthManager _plexOAuthManager;
|
||||||
private readonly ILogger<TokenController> _log;
|
private readonly ILogger<TokenController> _log;
|
||||||
|
private readonly ISettingsService<AuthenticationSettings> _authSettings;
|
||||||
|
private readonly ISettingsService<CloudflareAuthenticationSettings> _cfsettings;
|
||||||
|
|
||||||
|
private CloudflareJWTJson _cfJson;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the token.
|
/// Gets the token.
|
||||||
|
@ -100,6 +112,7 @@ namespace Ombi.Controllers.V1
|
||||||
/// Returns the Token for the Ombi User if we can match the Plex user with a valid Ombi User
|
/// Returns the Token for the Ombi User if we can match the Plex user with a valid Ombi User
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("plextoken")]
|
[HttpPost("plextoken")]
|
||||||
|
[ProducesResponseType(200)]
|
||||||
[ProducesResponseType(401)]
|
[ProducesResponseType(401)]
|
||||||
[ProducesResponseType(400)]
|
[ProducesResponseType(400)]
|
||||||
public async Task<IActionResult> GetTokenWithPlexToken([FromBody] PlexTokenAuthentication model)
|
public async Task<IActionResult> GetTokenWithPlexToken([FromBody] PlexTokenAuthentication model)
|
||||||
|
@ -117,6 +130,70 @@ namespace Ombi.Controllers.V1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the Token for the Ombi User if we can match the Plex user with a valid Ombi User
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("cfAuth")]
|
||||||
|
[ProducesResponseType(401)]
|
||||||
|
[ProducesResponseType(400)]
|
||||||
|
public async Task<IActionResult> GetTokenWithCFJwt([FromHeader(Name="Cf-Access-Jwt-Assertion")] string CFJWTAuthentication)
|
||||||
|
{
|
||||||
|
if (!_authSettings.GetSettings().EnableCloudflareAccess) {
|
||||||
|
_log.LogError("EnableCloudflareAccess is disabled, API won't work!");
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String.IsNullOrEmpty(CFJWTAuthentication)) {
|
||||||
|
_log.LogWarning("Cf-Access-Jwt-Assertion Not found!");
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCFKeys();
|
||||||
|
|
||||||
|
IList<JsonWebKey> jwkList = new List<JsonWebKey>();
|
||||||
|
foreach (Dictionary<string,string> key in _cfJson.keys) {
|
||||||
|
jwkList.Add(JsonWebKey.Create(JsonSerializer.Serialize(key)));
|
||||||
|
}
|
||||||
|
|
||||||
|
CloudflareAuthenticationSettings cfSettings = _cfsettings.GetSettings();
|
||||||
|
|
||||||
|
TokenValidationParameters validationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidIssuer = cfSettings.issuer,
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidAudience = cfSettings.audience,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKeys = jwkList,
|
||||||
|
TryAllIssuerSigningKeys = true
|
||||||
|
};
|
||||||
|
string email = null;
|
||||||
|
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ClaimsPrincipal claimsP = handler.ValidateToken(CFJWTAuthentication, validationParameters, out var validatedSecurityToken);
|
||||||
|
email = claimsP.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress").Value;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
if (String.IsNullOrEmpty(email)) {
|
||||||
|
_log.LogError("cfJWT email not found!");
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
OmbiUser user = await _userManager.FindByEmailAsync(email);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.LogInformation(String.Format("Logging in user {0} with cfJWT", email));
|
||||||
|
return await CreateToken(false, user);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<IActionResult> CreateToken(bool rememberMe, OmbiUser user)
|
private async Task<IActionResult> CreateToken(bool rememberMe, OmbiUser user)
|
||||||
{
|
{
|
||||||
var roles = await _userManager.GetRolesAsync(user);
|
var roles = await _userManager.GetRolesAsync(user);
|
||||||
|
@ -272,5 +349,21 @@ namespace Ombi.Controllers.V1
|
||||||
|
|
||||||
return ip;
|
return ip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateCFKeys() {
|
||||||
|
if (_cfJson != null && (!((DateTime.UtcNow - _cfJson.lastUpdate).TotalDays > 7))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string certlink = _cfsettings.GetSettings().certlink;
|
||||||
|
Uri uriResult;
|
||||||
|
if ((!Uri.TryCreate(certlink, UriKind.Absolute, out uriResult)) || !(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps)) {
|
||||||
|
_log.LogError("Failed to parse certificate link as URI!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HttpClient client = new HttpClient();
|
||||||
|
Task<string> res = Task.Run<string>(async () => await client.GetStringAsync(certlink));
|
||||||
|
_cfJson = JsonSerializer.Deserialize<CloudflareJWTJson>(res.Result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
src/Ombi/Models/External/CloudflareJWTJson.cs
vendored
Normal file
13
src/Ombi/Models/External/CloudflareJWTJson.cs
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ombi.Models.External
|
||||||
|
{
|
||||||
|
public class CloudflareJWTJson
|
||||||
|
{
|
||||||
|
public IList<Dictionary<string, string>> keys { get; set; }
|
||||||
|
public Dictionary<string, string> public_cert { get; set; }
|
||||||
|
public IList<Dictionary<string, string>> public_certs { get; set; }
|
||||||
|
public DateTime lastUpdate { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,9 +6,11 @@
|
||||||
"RememberMe": "Remember Me",
|
"RememberMe": "Remember Me",
|
||||||
"SignInWith": "Sign in with {{appName}}",
|
"SignInWith": "Sign in with {{appName}}",
|
||||||
"SignInWithPlex": "Sign in with Plex",
|
"SignInWithPlex": "Sign in with Plex",
|
||||||
|
"SignInWithCF": "Sign in with Cloudflare",
|
||||||
"ForgottenPassword": "Forgot your password?",
|
"ForgottenPassword": "Forgot your password?",
|
||||||
"Errors": {
|
"Errors": {
|
||||||
"IncorrectCredentials": "Incorrect username or password"
|
"IncorrectCredentials": "Incorrect username or password",
|
||||||
|
"UnauthorizedCFAccount": "Ombi has no user with the same email as the Cloudflare account"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Common": {
|
"Common": {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue