mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-08-23 06:25: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,
|
||||
IEmbyApiFactory embyApi, ISettingsService<EmbySettings> embySettings,
|
||||
IJellyfinApiFactory jellyfinApi, ISettingsService<JellyfinSettings> jellyfinSettings,
|
||||
ISettingsService<AuthenticationSettings> auth)
|
||||
ISettingsService<AuthenticationSettings> auth, ISettingsService<CloudflareAuthenticationSettings> cfauth)
|
||||
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
|
||||
{
|
||||
_plexApi = plexApi;
|
||||
|
|
|
@ -13,5 +13,6 @@ namespace Ombi.Settings.Settings.Models
|
|||
public bool RequireNonAlphanumeric { get; set; }
|
||||
public bool RequireUppercase { get; set; }
|
||||
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 });
|
||||
}
|
||||
|
||||
public attemptCF(): Observable<any> {
|
||||
return this.http.get<any>(`${this.url}/cfAuth`);
|
||||
}
|
||||
|
||||
public getToken() {
|
||||
return this.jwtHelperService.tokenGetter();
|
||||
}
|
||||
|
|
|
@ -230,6 +230,13 @@ export interface IAuthenticationSettings extends ISettings {
|
|||
requireNonAlphanumeric: boolean;
|
||||
requireUppercase: boolean;
|
||||
enableOAuth: boolean;
|
||||
enableCloudflareAccess: boolean;
|
||||
}
|
||||
|
||||
export interface ICloudflareSettings extends ISettings {
|
||||
issuer: string;
|
||||
audience: string;
|
||||
certlink: string;
|
||||
}
|
||||
|
||||
export interface ICustomPage extends ISettings {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<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>
|
||||
<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)">
|
||||
|
||||
|
||||
|
@ -35,11 +35,15 @@
|
|||
</form>
|
||||
</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>
|
||||
|
||||
<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"><i class="fas fa-circle-notch fa-spin fa-fw"></i></span>
|
||||
</button>
|
||||
|
|
|
@ -34,6 +34,7 @@ export class LoginComponent implements OnDestroy, OnInit {
|
|||
public loginWithOmbi: boolean;
|
||||
public pinTimer: any;
|
||||
public oauthLoading: boolean;
|
||||
public cfLoading: boolean;
|
||||
|
||||
public get appName(): string {
|
||||
if (this.customizationSettings.applicationName) {
|
||||
|
@ -101,7 +102,6 @@ export class LoginComponent implements OnDestroy, OnInit {
|
|||
}
|
||||
|
||||
public ngOnInit() {
|
||||
|
||||
this.customziationFacade.settings$().subscribe(x => this.customizationSettings = x);
|
||||
|
||||
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() {
|
||||
if (this.oAuthWindow) {
|
||||
this.oAuthWindow.close();
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Observable } from "rxjs";
|
|||
import {
|
||||
IAbout,
|
||||
IAuthenticationSettings,
|
||||
ICloudflareSettings,
|
||||
ICouchPotatoSettings,
|
||||
ICronTestModel,
|
||||
ICronViewModelBody,
|
||||
|
@ -125,6 +126,10 @@ export class SettingsService extends ServiceHelpers {
|
|||
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> {
|
||||
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});
|
||||
}
|
||||
|
||||
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
|
||||
public getLandingPage(): Observable<ILandingPageSettings> {
|
||||
return this.http.get<ILandingPageSettings>(`${this.url}/LandingPage`, {headers: this.headers});
|
||||
|
|
|
@ -1,66 +1,111 @@
|
|||
<settings-menu></settings-menu>
|
||||
<div class="small-middle-container">
|
||||
<wiki></wiki>
|
||||
<fieldset *ngIf="form">
|
||||
<legend>Authentication</legend>
|
||||
<div class="md-form-field" style="margin-top:1em;"></div>
|
||||
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<mat-slide-toggle id="allowNoPassword" name="allowNoPassword" formControlName="allowNoPassword">
|
||||
Allow users to login without a password</mat-slide-toggle>
|
||||
<wiki></wiki>
|
||||
<fieldset *ngIf="form">
|
||||
<legend>Authentication</legend>
|
||||
<div class="md-form-field" style="margin-top:1em;"></div>
|
||||
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<mat-slide-toggle id="allowNoPassword" name="allowNoPassword" formControlName="allowNoPassword">
|
||||
Allow users to login without a password</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<mat-slide-toggle id="enableOAuth" name="enableOAuth" formControlName="enableOAuth">Enable Plex OAuth</mat-slide-toggle>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<mat-slide-toggle id="enableOAuth" name="enableOAuth" formControlName="enableOAuth">Enable Plex OAuth</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <hr/>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" id="requiredDigit" name="requiredDigit" formControlName="requiredDigit">
|
||||
<label for="requiredDigit">Require a digit in the password</label>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="requiredLength" class="control-label">Required password length</label>
|
||||
<div>
|
||||
<input type="text" class="form-control form-control-custom " id="requiredLength" name="requiredLength" formControlName="requiredLength">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" id="requiredLowercase" name="requiredLowercase" formControlName="requiredLowercase">
|
||||
<label for="requiredLowercase">Require a lowercase character in the password</label>
|
||||
<!-- <hr/>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" id="requiredDigit" name="requiredDigit" formControlName="requiredDigit">
|
||||
<label for="requiredDigit">Require a digit in the password</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" id="requireNonAlphanumeric" name="requireNonAlphanumeric" formControlName="requireNonAlphanumeric">
|
||||
<label for="requireNonAlphanumeric">Require a NonAlphanumeric character in the password</label>
|
||||
<div class="form-group">
|
||||
<label for="requiredLength" class="control-label">Required password length</label>
|
||||
<div>
|
||||
<input type="text" class="form-control form-control-custom " id="requiredLength" name="requiredLength" formControlName="requiredLength">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" id="requireUppercase" name="requireUppercase" formControlName="requireUppercase">
|
||||
<label for="requireUppercase">Require a uppercase character in the password</label>
|
||||
</div>
|
||||
</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 class="form-group">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" id="requiredLowercase" name="requiredLowercase" formControlName="requiredLowercase">
|
||||
<label for="requiredLowercase">Require a lowercase character in the password</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" id="requireNonAlphanumeric" name="requireNonAlphanumeric" formControlName="requireNonAlphanumeric">
|
||||
<label for="requireNonAlphanumeric">Require a NonAlphanumeric character in the password</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" id="requireUppercase" name="requireUppercase" formControlName="requireUppercase">
|
||||
<label for="requireUppercase">Require a uppercase character in the password</label>
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
</form>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
</div>
|
|
@ -11,6 +11,7 @@ import { SettingsService } from "../../services";
|
|||
export class AuthenticationComponent implements OnInit {
|
||||
|
||||
public form: FormGroup;
|
||||
public cfform: FormGroup;
|
||||
|
||||
constructor(private settingsService: SettingsService,
|
||||
private notificationService: NotificationService,
|
||||
|
@ -26,6 +27,14 @@ export class AuthenticationComponent implements OnInit {
|
|||
requireNonAlphanumeric: [x.requireNonAlphanumeric],
|
||||
requireUppercase: [x.requireUppercase],
|
||||
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;
|
||||
color: $ombi-active-text;
|
||||
}
|
||||
&#sign-cf{
|
||||
background-color: #282A2D;
|
||||
color: #E5A00D;
|
||||
}
|
||||
&#sign-plex{
|
||||
background-color: #282A2D;
|
||||
color: #E5A00D;
|
||||
|
|
|
@ -467,6 +467,39 @@ namespace Ombi.Controllers.V1
|
|||
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>
|
||||
/// Save the Radarr settings.
|
||||
/// </summary>
|
||||
|
|
|
@ -2,18 +2,23 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Ombi.Core.Authentication;
|
||||
using Ombi.Core.Settings;
|
||||
using Ombi.Helpers;
|
||||
using Ombi.Models;
|
||||
using Ombi.Models.External;
|
||||
using Ombi.Models.Identity;
|
||||
using Ombi.Settings.Settings.Models;
|
||||
using Ombi.Store.Entities;
|
||||
using Ombi.Store.Repository;
|
||||
|
||||
|
@ -25,13 +30,16 @@ namespace Ombi.Controllers.V1
|
|||
public class TokenController : ControllerBase
|
||||
{
|
||||
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;
|
||||
_tokenAuthenticationOptions = ta.Value;
|
||||
_token = token;
|
||||
_plexOAuthManager = oAuthManager;
|
||||
_log = logger;
|
||||
_authSettings = auth;
|
||||
_cfsettings = cfauth;
|
||||
}
|
||||
|
||||
private readonly TokenAuthentication _tokenAuthenticationOptions;
|
||||
|
@ -39,6 +47,10 @@ namespace Ombi.Controllers.V1
|
|||
private readonly OmbiUserManager _userManager;
|
||||
private readonly IPlexOAuthManager _plexOAuthManager;
|
||||
private readonly ILogger<TokenController> _log;
|
||||
private readonly ISettingsService<AuthenticationSettings> _authSettings;
|
||||
private readonly ISettingsService<CloudflareAuthenticationSettings> _cfsettings;
|
||||
|
||||
private CloudflareJWTJson _cfJson;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
[HttpPost("plextoken")]
|
||||
[ProducesResponseType(200)]
|
||||
[ProducesResponseType(401)]
|
||||
[ProducesResponseType(400)]
|
||||
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)
|
||||
{
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
|
@ -272,5 +349,21 @@ namespace Ombi.Controllers.V1
|
|||
|
||||
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",
|
||||
"SignInWith": "Sign in with {{appName}}",
|
||||
"SignInWithPlex": "Sign in with Plex",
|
||||
"SignInWithCF": "Sign in with Cloudflare",
|
||||
"ForgottenPassword": "Forgot your password?",
|
||||
"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": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue