Merge pull request #2174 from tidusjar/feature/plexoauth

Added Plex OAuth into Ombi
pull/2181/head
Jamie 7 years ago committed by GitHub
commit 4e8daeecd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,8 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.Friends;
using Ombi.Api.Plex.Models.OAuth;
using Ombi.Api.Plex.Models.Server;
using Ombi.Api.Plex.Models.Status;
@ -20,5 +22,8 @@ namespace Ombi.Api.Plex
Task<PlexFriends> GetUsers(string authToken);
Task<PlexAccount> GetAccount(string authToken);
Task<PlexMetadata> GetRecentlyAdded(string authToken, string uri, string sectionId);
Task<OAuthPin> CreatePin();
Task<OAuthPin> GetPin(int pinId);
Uri GetOAuthUrl(int pinId, string code, string applicationUrl, bool wizard);
}
}

@ -0,0 +1,27 @@
using System;
namespace Ombi.Api.Plex.Models.OAuth
{
public class OAuthPin
{
public int id { get; set; }
public string code { get; set; }
public bool trusted { get; set; }
public string clientIdentifier { get; set; }
public Location location { get; set; }
public int expiresIn { get; set; }
public DateTime createdAt { get; set; }
public DateTime expiresAt { get; set; }
public string authToken { get; set; }
}
public class Location
{
public string code { get; set; }
public string country { get; set; }
public string city { get; set; }
public string subdivisions { get; set; }
public string coordinates { get; set; }
}
}

@ -1,20 +1,53 @@
using System.Net.Http;
using System;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.Friends;
using Ombi.Api.Plex.Models.OAuth;
using Ombi.Api.Plex.Models.Server;
using Ombi.Api.Plex.Models.Status;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
namespace Ombi.Api.Plex
{
public class PlexApi : IPlexApi
{
public PlexApi(IApi api)
public PlexApi(IApi api, ISettingsService<CustomizationSettings> settings)
{
Api = api;
_custom = settings;
}
private IApi Api { get; }
private readonly ISettingsService<CustomizationSettings> _custom;
private string _app;
private string ApplicationName
{
get
{
if (string.IsNullOrEmpty(_app))
{
var settings = _custom.GetSettings();
if (settings.ApplicationName.IsNullOrEmpty())
{
_app = "Ombi";
}
else
{
_app = settings.ApplicationName;
}
return _app;
}
return _app;
}
}
private const string SignInUri = "https://plex.tv/users/sign_in.json";
private const string FriendsUri = "https://plex.tv/pms/friends/all";
@ -156,6 +189,50 @@ namespace Ombi.Api.Plex
return await Api.Request<PlexMetadata>(request);
}
public async Task<OAuthPin> CreatePin()
{
var request = new Request($"api/v2/pins", "https://plex.tv/", HttpMethod.Post);
request.AddQueryString("strong", "true");
AddHeaders(request);
return await Api.Request<OAuthPin>(request);
}
public async Task<OAuthPin> GetPin(int pinId)
{
var request = new Request($"api/v2/pins/{pinId}", "https://plex.tv/", HttpMethod.Get);
AddHeaders(request);
return await Api.Request<OAuthPin>(request);
}
public Uri GetOAuthUrl(int pinId, string code, string applicationUrl, bool wizard)
{
var request = new Request("auth#", "https://app.plex.tv", HttpMethod.Get);
AddHeaders(request);
var forwardUrl = wizard
? new Request($"Wizard/OAuth/{pinId}", applicationUrl, HttpMethod.Get)
: new Request($"Login/OAuth/{pinId}", applicationUrl, HttpMethod.Get);
request.AddQueryString("forwardUrl", forwardUrl.FullUri.ToString());
request.AddQueryString("pinID", pinId.ToString());
request.AddQueryString("code", code);
request.AddQueryString("context[device][product]", "Ombi");
request.AddQueryString("context[device][environment]", "bundled");
request.AddQueryString("clientID", $"OmbiV3");
if (request.FullUri.Fragment.Equals("#"))
{
var uri = request.FullUri.ToString();
var withoutEnd = uri.Remove(uri.Length - 1, 1);
var startOfQueryLocation = withoutEnd.IndexOf('?');
var better = withoutEnd.Insert(startOfQueryLocation, "#");
request.FullUri = new Uri(better);
}
return request.FullUri;
}
/// <summary>
/// Adds the required headers and also the authorization header
/// </summary>
@ -174,7 +251,7 @@ namespace Ombi.Api.Plex
private void AddHeaders(Request request)
{
request.AddHeader("X-Plex-Client-Identifier", $"OmbiV3");
request.AddHeader("X-Plex-Product", "Ombi");
request.AddHeader("X-Plex-Product", ApplicationName);
request.AddHeader("X-Plex-Version", "3");
request.AddContentHeader("Content-Type", request.ContentType == ContentType.Json ? "application/json" : "application/xml");
request.AddHeader("Accept", "application/json");

@ -10,7 +10,7 @@ namespace Ombi.Api
{
public Request()
{
}
public Request(string endpoint, string baseUrl, HttpMethod http, ContentType contentType = ContentType.Json)
@ -105,10 +105,10 @@ namespace Ombi.Api
hasQuery = true;
startingTag = builder.Query.Contains("?") ? "&" : "?";
}
builder.Query = hasQuery
? $"{builder.Query}{startingTag}{key}={value}"
: $"{startingTag}{key}={value}";
_modified = builder.Uri;
}

@ -0,0 +1,76 @@
using System;
using System.Threading.Tasks;
using Ombi.Api.Plex;
using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.OAuth;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
namespace Ombi.Core.Authentication
{
public class PlexOAuthManager : IPlexOAuthManager
{
public PlexOAuthManager(IPlexApi api, ISettingsService<CustomizationSettings> settings)
{
_api = api;
_customizationSettingsService = settings;
}
private readonly IPlexApi _api;
private readonly ISettingsService<CustomizationSettings> _customizationSettingsService;
public async Task<OAuthPin> RequestPin()
{
var pin = await _api.CreatePin();
return pin;
}
public async Task<string> GetAccessTokenFromPin(int pinId)
{
var pin = await _api.GetPin(pinId);
if (pin.expiresAt < DateTime.UtcNow)
{
return string.Empty;
}
if (pin.authToken.IsNullOrEmpty())
{
// Looks like we do not have a pin yet, we should retry a few times.
var retryCount = 0;
var retryMax = 5;
var retryWaitMs = 1000;
while (pin.authToken.IsNullOrEmpty() && retryCount < retryMax)
{
retryCount++;
await Task.Delay(retryWaitMs);
pin = await _api.GetPin(pinId);
}
}
return pin.authToken;
}
public async Task<PlexAccount> GetAccount(string accessToken)
{
return await _api.GetAccount(accessToken);
}
public async Task<Uri> GetOAuthUrl(int pinId, string code)
{
var settings = await _customizationSettingsService.GetSettingsAsync();
if (settings.ApplicationUrl.IsNullOrEmpty())
{
return null;
}
var url = _api.GetOAuthUrl(pinId, code, settings.ApplicationUrl, false);
return url;
}
public Uri GetWizardOAuthUrl(int pinId, string code, string websiteAddress)
{
var url = _api.GetOAuthUrl(pinId, code, websiteAddress, true);
return url;
}
}
}

@ -0,0 +1,16 @@
using System;
using System.Threading.Tasks;
using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.OAuth;
namespace Ombi.Core.Authentication
{
public interface IPlexOAuthManager
{
Task<string> GetAccessTokenFromPin(int pinId);
Task<OAuthPin> RequestPin();
Task<Uri> GetOAuthUrl(int pinId, string code);
Uri GetWizardOAuthUrl(int pinId, string code, string websiteAddress);
Task<PlexAccount> GetAccount(string accessToken);
}
}

@ -51,6 +51,7 @@ using Ombi.Store.Repository.Requests;
using Ombi.Updater;
using PlexContentCacher = Ombi.Schedule.Jobs.Plex;
using Ombi.Api.Telegram;
using Ombi.Core.Authentication;
using Ombi.Core.Processor;
using Ombi.Schedule.Jobs.Plex.Interfaces;
using Ombi.Schedule.Jobs.SickRage;
@ -82,6 +83,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IRecentlyAddedEngine, RecentlyAddedEngine>();
services.AddTransient<ITvSender, TvSender>();
services.AddTransient<IMassEmailSender, MassEmailSender>();
services.AddTransient<IPlexOAuthManager, PlexOAuthManager>();
}
public static void RegisterHttp(this IServiceCollection services)
{

@ -7,7 +7,6 @@ namespace Ombi.Core.Settings.Models.External
{
public bool Enable { get; set; }
public List<PlexServers> Servers { get; set; }
}
public class PlexServers : ExternalSettings

@ -24,6 +24,7 @@ import { CookieComponent } from "./auth/cookie.component";
import { PageNotFoundComponent } from "./errors/not-found.component";
import { LandingPageComponent } from "./landingpage/landingpage.component";
import { LoginComponent } from "./login/login.component";
import { LoginOAuthComponent } from "./login/loginoauth.component";
import { ResetPasswordComponent } from "./login/resetpassword.component";
import { TokenResetPasswordComponent } from "./login/tokenresetpassword.component";
@ -41,6 +42,7 @@ const routes: Routes = [
{ path: "*", component: PageNotFoundComponent },
{ path: "", redirectTo: "/search", pathMatch: "full" },
{ path: "login", component: LoginComponent },
{ path: "Login/OAuth/:pin", component: LoginOAuthComponent },
{ path: "login/:landing", component: LoginComponent },
{ path: "reset", component: ResetPasswordComponent },
{ path: "token", component: TokenResetPasswordComponent },
@ -116,6 +118,7 @@ export function HttpLoaderFactory(http: HttpClient, platformLocation: PlatformLo
ResetPasswordComponent,
TokenResetPasswordComponent,
CookieComponent,
LoginOAuthComponent,
],
providers: [
NotificationService,

@ -2,6 +2,7 @@
username: string;
password: string;
rememberMe: boolean;
usePlexOAuth: boolean;
}
export interface ILocalUser {

@ -18,6 +18,10 @@ export class AuthService extends ServiceHelpers {
return this.http.post(`${this.url}/`, JSON.stringify(login), {headers: this.headers});
}
public oAuth(pin: number): Observable<any> {
return this.http.get<any>(`${this.url}/${pin}`, {headers: this.headers});
}
public requiresPassword(login: IUserLogin): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/requirePassword`, JSON.stringify(login), {headers: this.headers});
}

@ -2,6 +2,10 @@
user: IPlexUser;
}
export interface IPlexOAuthAccessToken {
accessToken: string;
}
export interface IPlexUser {
email: string;
uuid: string;

@ -7,11 +7,13 @@ include the remember me checkbox
<div *ngIf="background" @fadeInOut class="bg" [style.background-image]="background"></div>
<div class="container" id="login">
<div class="card card-container">
<!-- <img class="profile-img-card" src="//lh3.googleusercontent.com/-6V8xOA6M7BA/AAAAAAAAAAI/AAAAAAAAAAA/rzlHcD0KYwo/photo.jpg?sz=120" alt="" /> -->
<div *ngIf="!customizationSettings.logo"><img id="profile-img" class="profile-img-card" src="{{baseUrl}}/images/logo.png"/></div>
<div *ngIf="customizationSettings.logo"><img id="profile-img" class="center" [src]="customizationSettings.logo" /></div>
<p id="profile-name" class="profile-name-card"></p>
<div *ngIf="showLoginForm && loginWithOmbi">
<form *ngIf="authenticationSettings" class="form-signin" novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
@ -27,6 +29,16 @@ include the remember me checkbox
</div>
<button class="btn btn-success" type="submit" [translate]="'Login.SignInButton'"></button>
</form><!-- /form -->
</div>
<div class="form-signin" *ngIf="plexEnabled && customizationSettings.applicationUrl && !loginWithOmbi">
<button class="btn btn-success" type="button" (click)="loginWithOmbi = true">
Continue With {{appName}}</button>
</div>
<div class="form-signin" *ngIf="plexEnabled && customizationSettings.applicationUrl && !loginWithOmbi">
<button class="btn btn-primary" type="button" (click)="oauth()">
Continue With Plex</button>
</div>
<a [routerLink]="['/reset']" class="forgot-password col-md-12">
<b [translate]="'Login.ForgottenPassword'"></b>
</a>

@ -25,9 +25,38 @@ export class LoginComponent implements OnDestroy, OnInit {
public form: FormGroup;
public customizationSettings: ICustomizationSettings;
public authenticationSettings: IAuthenticationSettings;
public plexEnabled: boolean;
public background: any;
public landingFlag: boolean;
public baseUrl: string;
public get showLoginForm(): boolean {
if(this.customizationSettings.applicationUrl && this.plexEnabled) {
this.loginWithOmbi = false;
return false;
}
if(!this.customizationSettings.applicationUrl || !this.plexEnabled) {
this.loginWithOmbi = true;
return true;
}
if(this.loginWithOmbi) {
return true;
}
this.loginWithOmbi = true;
return true;
}
public loginWithOmbi: boolean = false;
public get appName(): string {
if(this.customizationSettings.applicationName) {
return this.customizationSettings.applicationName;
} else {
return "Ombi";
}
}
private timer: any;
private errorBody: string;
@ -68,6 +97,7 @@ export class LoginComponent implements OnDestroy, OnInit {
public ngOnInit() {
this.settingsService.getAuthentication().subscribe(x => this.authenticationSettings = x);
this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x);
this.settingsService.getStatusPlex().subscribe(x => this.plexEnabled = x);
this.images.getRandomBackground().subscribe(x => {
this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.7) 20.0%, rgba(0,0,0,0.7) 80.0%, transparent 80%),url(" + x.url + ")");
});
@ -90,7 +120,7 @@ export class LoginComponent implements OnDestroy, OnInit {
return;
}
const value = form.value;
const user = { password: value.password, username: value.username, rememberMe:value.rememberMe };
const user = { password: value.password, username: value.username, rememberMe: value.rememberMe, usePlexOAuth: false };
this.authService.requiresPassword(user).subscribe(x => {
if(x && this.authenticationSettings.allowNoPassword) {
// Looks like this user requires a password
@ -111,6 +141,12 @@ export class LoginComponent implements OnDestroy, OnInit {
});
}
public oauth() {
this.authService.login({usePlexOAuth: true, password:"",rememberMe:true,username:""}).subscribe(x => {
window.location.href = x.url;
});
}
public ngOnDestroy() {
clearInterval(this.timer);
}
@ -124,5 +160,4 @@ export class LoginComponent implements OnDestroy, OnInit {
.bypassSecurityTrustStyle("linear-gradient(-10deg, transparent 20%, rgba(0,0,0,0.7) 20.0%, rgba(0,0,0,0.7) 80.0%, transparent 80%), url(" + x.url + ")");
});
}
}

@ -0,0 +1,9 @@

<div >
<div class="container" id="login">
<div class="card card-container">
<label>Please Wait...</label>
</div><!-- /card-container -->
</div><!-- /container -->
</div>

@ -0,0 +1,35 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { AuthService } from "../auth/auth.service";
@Component({
templateUrl: "./loginoauth.component.html",
})
export class LoginOAuthComponent implements OnInit {
public pin: number;
constructor(private authService: AuthService, private router: Router,
private route: ActivatedRoute) {
this.route.params
.subscribe((params: any) => {
this.pin = params.pin;
});
}
public ngOnInit(): void {
this.auth();
}
public auth() {
this.authService.oAuth(this.pin).subscribe(x => {
localStorage.setItem("id_token", x.access_token);
if (this.authService.loggedIn()) {
this.router.navigate(["search"]);
}
});
}
}

@ -4,3 +4,4 @@ export * from "./plex.service";
export * from "./radarr.service";
export * from "./sonarr.service";
export * from "./tester.service";
export * from "./plexoauth.service";

@ -29,4 +29,8 @@ export class PlexService extends ServiceHelpers {
public getFriends(): Observable<IUsersModel[]> {
return this.http.get<IUsersModel[]>(`${this.url}Friends`, {headers: this.headers});
}
public oAuth(wizard: boolean): Observable<any> {
return this.http.get<any>(`${this.url}oauth/${wizard}`, {headers: this.headers});
}
}

@ -0,0 +1,20 @@
import { PlatformLocation } from "@angular/common";
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx";
import { ServiceHelpers } from "../service.helpers";
import { IPlexOAuthAccessToken } from "../../interfaces";
@Injectable()
export class PlexOAuthService extends ServiceHelpers {
constructor(http: HttpClient, public platformLocation: PlatformLocation) {
super(http, "/api/v1/PlexOAuth/", platformLocation);
}
public oAuth(pin: number): Observable<IPlexOAuthAccessToken> {
return this.http.get<IPlexOAuthAccessToken>(`${this.url}${pin}`, {headers: this.headers});
}
}

@ -71,6 +71,10 @@ export class SettingsService extends ServiceHelpers {
return this.http.get<IPlexSettings>(`${this.url}/Plex/`, {headers: this.headers});
}
public getStatusPlex(): Observable<boolean> {
return this.http.get<boolean>(`${this.url}/Plexstatus/`, {headers: this.headers});
}
public savePlex(settings: IPlexSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/Plex/`, JSON.stringify(settings), {headers: this.headers});
}

@ -21,7 +21,7 @@ export class CreateAdminComponent {
this.identityService.createWizardUser({username: this.username, password: this.password, usePlexAdminAccount: false}).subscribe(x => {
if (x) {
// Log me in.
this.auth.login({ username: this.username, password: this.password, rememberMe:false }).subscribe(c => {
this.auth.login({ username: this.username, password: this.password, rememberMe: false, usePlexOAuth:false }).subscribe(c => {
localStorage.setItem("id_token", c.access_token);

@ -17,9 +17,16 @@
<small>Please note we do not store this information, we only store your Plex Authorization Token that will allow Ombi to view your media and friends</small>
<div class="form-group">
<div style="text-align: center; margin-top: 20px">
<button (click)="requestAuthToken()" class="btn btn-primary-outline">Request Token <i class="fa fa-key"></i></button>
<button (click)="requestAuthToken()" class="btn btn-success-outline">Request Token <i class="fa fa-key"></i></button>
</div>
</div>
<p class="text-center">OR</p>
<div class="form-group">
<div style="text-align: center; margin-top: 20px">
<button (click)="oauth()" class="btn btn-primary" type="button">Continue With Plex</button>
</div>
</div>
</div>
</div>
</div>

@ -1,8 +1,6 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { ConfirmationService } from "primeng/primeng";
import { PlexService } from "../../services";
import { IdentityService, NotificationService, SettingsService } from "../../services";
import { AuthService } from "./../../auth/auth.service";
@ -17,7 +15,6 @@ export class PlexComponent {
constructor(private plexService: PlexService, private router: Router,
private notificationService: NotificationService,
private confirmationService: ConfirmationService,
private identityService: IdentityService,
private settings: SettingsService,
private auth: AuthService) { }
@ -28,25 +25,21 @@ export class PlexComponent {
this.notificationService.error("Username or password was incorrect. Could not authenticate with Plex.");
return;
}
this.confirmationService.confirm({
message: "Do you want your Plex user to be the main admin account on Ombi?",
header: "Use Plex Account",
icon: "fa fa-check",
accept: () => {
this.identityService.createWizardUser({
this.identityService.createWizardUser({
username: "",
password: "",
usePlexAdminAccount: true,
}).subscribe(x => {
if (x) {
this.auth.login({ username: this.login, password: this.password, rememberMe:false }).subscribe(c => {
}).subscribe(y => {
if (y) {
this.auth.login({ username: this.login, password: this.password, rememberMe: false, usePlexOAuth: false }).subscribe(c => {
localStorage.setItem("id_token", c.access_token);
// Mark that we have done the settings now
this.settings.getOmbi().subscribe(ombi => {
ombi.wizard = true;
this.settings.saveOmbi(ombi).subscribe(x => {
this.settings.saveOmbi(ombi).subscribe(s => {
this.settings.getUserManagementSettings().subscribe(usr => {
usr.importPlexAdmin = true;
@ -64,10 +57,14 @@ export class PlexComponent {
}
});
},
reject: () => {
this.router.navigate(["Wizard/CreateAdmin"]);
},
});
);
}
public oauth() {
this.plexService.oAuth(true).subscribe(x => {
if(x.url) {
window.location.href = x.url;
}
});
}
}

@ -0,0 +1,14 @@

<img class="landing-header" src="/images/logo.png" width="300" />
<div class="landing-block shadow">
<div class="media">
<div id="contentBody" class="media-body">
<h4 class="media-heading landing-title">Plex Authentication</h4>
<div class="form-group">
<label for="username" class="control-label">Please Wait</label>
</div>
</div>
</div>
</div>
<p-confirmDialog></p-confirmDialog>

@ -0,0 +1,67 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { IdentityService, PlexOAuthService, SettingsService } from "../../services";
import { AuthService } from "./../../auth/auth.service";
@Component({
templateUrl: "./plexoauth.component.html",
})
export class PlexOAuthComponent implements OnInit {
public pinId: number;
constructor(private route: ActivatedRoute,
private plexOauth: PlexOAuthService,
private identityService: IdentityService,
private settings: SettingsService,
private router: Router,
private auth: AuthService) {
this.route.params
.subscribe((params: any) => {
this.pinId = params.pin;
});
}
public ngOnInit(): void {
this.plexOauth.oAuth(this.pinId).subscribe(x => {
if(!x.accessToken) {
return;
// RETURN
}
this.identityService.createWizardUser({
username: "",
password: "",
usePlexAdminAccount: true,
}).subscribe(u => {
if (u) {
this.auth.oAuth(this.pinId).subscribe(c => {
localStorage.setItem("id_token", c.access_token);
// Mark that we have done the settings now
this.settings.getOmbi().subscribe(ombi => {
ombi.wizard = true;
this.settings.saveOmbi(ombi).subscribe(s => {
this.settings.getUserManagementSettings().subscribe(usr => {
usr.importPlexAdmin = true;
this.settings.saveUserManagementSettings(usr).subscribe(saved => {
this.router.navigate(["login"]);
});
});
});
});
});
} else {
//this.notificationService.error("Could not get the Plex Admin Information");
return;
}
});
});
}
}

@ -14,6 +14,8 @@ import { WelcomeComponent } from "./welcome/welcome.component";
import { EmbyService } from "../services";
import { PlexService } from "../services";
import { IdentityService } from "../services";
import { PlexOAuthService } from "../services";
import { PlexOAuthComponent } from "./plex/plexoauth.component";
const routes: Routes = [
{ path: "", component: WelcomeComponent},
@ -21,6 +23,7 @@ const routes: Routes = [
{ path: "Plex", component: PlexComponent},
{ path: "Emby", component: EmbyComponent},
{ path: "CreateAdmin", component: CreateAdminComponent},
{ path: "OAuth/:pin", component: PlexOAuthComponent},
];
@NgModule({
imports: [
@ -33,6 +36,7 @@ const routes: Routes = [
WelcomeComponent,
MediaServerComponent,
PlexComponent,
PlexOAuthComponent,
CreateAdminComponent,
EmbyComponent,
],
@ -44,6 +48,7 @@ const routes: Routes = [
IdentityService,
EmbyService,
ConfirmationService,
PlexOAuthService,
],
})

@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
using Ombi.Api.Plex;
using Ombi.Api.Plex.Models;
using Ombi.Attributes;
using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
@ -21,16 +22,18 @@ namespace Ombi.Controllers.External
public class PlexController : Controller
{
public PlexController(IPlexApi plexApi, ISettingsService<PlexSettings> plexSettings,
ILogger<PlexController> logger)
ILogger<PlexController> logger, IPlexOAuthManager manager)
{
PlexApi = plexApi;
PlexSettings = plexSettings;
_log = logger;
_plexOAuthManager = manager;
}
private IPlexApi PlexApi { get; }
private ISettingsService<PlexSettings> PlexSettings { get; }
private readonly ILogger<PlexController> _log;
private readonly IPlexOAuthManager _plexOAuthManager;
/// <summary>
/// Signs into the Plex API.
@ -173,5 +176,37 @@ namespace Ombi.Controllers.External
// Filter out any dupes
return vm.DistinctBy(x => x.Id);
}
[HttpGet("oauth/{wizard:bool}")]
[AllowAnonymous]
public async Task<IActionResult> OAuth(bool wizard)
{
//https://app.plex.tv/auth#?forwardUrl=http://google.com/&clientID=Ombi-Test&context%5Bdevice%5D%5Bproduct%5D=Ombi%20SSO&pinID=798798&code=4lgfd
// Plex OAuth
// Redirect them to Plex
// We need a PIN first
var pin = await _plexOAuthManager.RequestPin();
Uri url;
if (!wizard)
{
url = await _plexOAuthManager.GetOAuthUrl(pin.id, pin.code);
}
else
{
var websiteAddress =$"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}";
url = _plexOAuthManager.GetWizardOAuthUrl(pin.id, pin.code, websiteAddress);
}
if (url == null)
{
return new JsonResult(new
{
error = "Application URL has not been set"
});
}
return new JsonResult(new {url = url.ToString()});
}
}
}

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http.Internal;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.Plex;
using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
namespace Ombi.Controllers
{
[ApiExplorerSettings(IgnoreApi = true)]
[ApiV1]
[AllowAnonymous]
public class PlexOAuthController : Controller
{
public PlexOAuthController(IPlexOAuthManager manager, IPlexApi plexApi, ISettingsService<PlexSettings> plexSettings,
ILogger<PlexOAuthController> log)
{
_manager = manager;
_plexApi = plexApi;
_plexSettings = plexSettings;
_log = log;
}
private readonly IPlexOAuthManager _manager;
private readonly IPlexApi _plexApi;
private readonly ISettingsService<PlexSettings> _plexSettings;
private readonly ILogger _log;
[HttpGet("{pinId:int}")]
public async Task<IActionResult> OAuthWizardCallBack([FromRoute] int pinId)
{
var accessToken = await _manager.GetAccessTokenFromPin(pinId);
if (accessToken.IsNullOrEmpty())
{
return Json(new
{
success = false,
error = "Authentication did not work. Please try again"
});
}
var settings = await _plexSettings.GetSettingsAsync();
var server = await _plexApi.GetServer(accessToken);
var servers = server.Server.FirstOrDefault();
if (servers == null)
{
_log.LogWarning("Looks like we can't find any Plex Servers");
}
_log.LogDebug("Adding first server");
settings.Enable = true;
settings.Servers = new List<PlexServers> {
new PlexServers
{
PlexAuthToken = accessToken,
Id = new Random().Next(),
Ip = servers?.LocalAddresses?.Split(new []{','}, StringSplitOptions.RemoveEmptyEntries)?.FirstOrDefault() ?? string.Empty,
MachineIdentifier = servers?.MachineIdentifier ?? string.Empty,
Port = int.Parse(servers?.Port ?? "0"),
Ssl = (servers?.Scheme ?? "http") != "http",
Name = "Server 1",
}
};
await _plexSettings.SaveSettingsAsync(settings);
return Json(new { accessToken });
}
}
}

@ -142,7 +142,19 @@ namespace Ombi.Controllers
[HttpGet("plex")]
public async Task<PlexSettings> PlexSettings()
{
return await Get<PlexSettings>();
var s = await Get<PlexSettings>();
return s;
}
[HttpGet("plexstatus")]
[AllowAnonymous]
public async Task<bool> PlexStatusSettings()
{
var s = await Get<PlexSettings>();
return s.Enable;
}
/// <summary>

@ -2,6 +2,7 @@
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.Threading.Tasks;
@ -9,6 +10,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Ombi.Api;
using Ombi.Core.Authentication;
using Ombi.Helpers;
using Ombi.Models;
@ -23,18 +25,21 @@ namespace Ombi.Controllers
[Produces("application/json")]
public class TokenController
{
public TokenController(OmbiUserManager um, IOptions<TokenAuthentication> ta, IAuditRepository audit, ITokenRepository token)
public TokenController(OmbiUserManager um, IOptions<TokenAuthentication> ta, IAuditRepository audit, ITokenRepository token,
IPlexOAuthManager oAuthManager)
{
_userManager = um;
_tokenAuthenticationOptions = ta.Value;
_audit = audit;
_token = token;
_plexOAuthManager = oAuthManager;
}
private readonly TokenAuthentication _tokenAuthenticationOptions;
private readonly IAuditRepository _audit;
private readonly ITokenRepository _token;
private readonly OmbiUserManager _userManager;
private readonly IPlexOAuthManager _plexOAuthManager;
/// <summary>
/// Gets the token.
@ -44,71 +49,123 @@ namespace Ombi.Controllers
[HttpPost]
public async Task<IActionResult> GetToken([FromBody] UserAuthModel model)
{
await _audit.Record(AuditType.None, AuditArea.Authentication,
if (!model.UsePlexOAuth)
{
await _audit.Record(AuditType.None, AuditArea.Authentication,
$"UserName {model.Username} attempting to authenticate");
var user = await _userManager.FindByNameAsync(model.Username);
if (user == null)
{
// Could this be an email login?
user = await _userManager.FindByEmailAsync(model.Username);
var user = await _userManager.FindByNameAsync(model.Username);
if (user == null)
{
return new UnauthorizedResult();
}
// Could this be an email login?
user = await _userManager.FindByEmailAsync(model.Username);
user.EmailLogin = true;
}
if (user == null)
{
return new UnauthorizedResult();
}
// Verify Password
if (await _userManager.CheckPasswordAsync(user, model.Password))
{
var roles = await _userManager.GetRolesAsync(user);
user.EmailLogin = true;
}
if (roles.Contains(OmbiRoles.Disabled))
// Verify Password
if (await _userManager.CheckPasswordAsync(user, model.Password))
{
return new UnauthorizedResult();
return await CreateToken(model.RememberMe, user);
}
user.LastLoggedIn = DateTime.UtcNow;
await _userManager.UpdateAsync(user);
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Name, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
claims.AddRange(roles.Select(role => new Claim("role", role)));
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenAuthenticationOptions.SecretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
claims: claims,
expires: model.RememberMe ? DateTime.UtcNow.AddDays(7) : DateTime.UtcNow.AddHours(5),
signingCredentials: creds,
audience: "Ombi", issuer:"Ombi"
);
var accessToken = new JwtSecurityTokenHandler().WriteToken(token);
if (model.RememberMe)
}
else
{
// Plex OAuth
// Redirect them to Plex
// We need a PIN first
var pin = await _plexOAuthManager.RequestPin();
//https://app.plex.tv/auth#?forwardUrl=http://google.com/&clientID=Ombi-Test&context%5Bdevice%5D%5Bproduct%5D=Ombi%20SSO&pinID=798798&code=4lgfd
var url = await _plexOAuthManager.GetOAuthUrl(pin.id, pin.code);
if (url == null)
{
// Save the token so we can refresh it later
//await _token.CreateToken(new Tokens() {Token = accessToken, User = user});
return new JsonResult(new
{
error = "Application URL has not been set"
});
}
return new JsonResult(new { url = url.ToString() });
}
return new UnauthorizedResult();
}
private async Task<IActionResult> CreateToken(bool rememberMe, OmbiUser user)
{
var roles = await _userManager.GetRolesAsync(user);
if (roles.Contains(OmbiRoles.Disabled))
{
return new UnauthorizedResult();
}
user.LastLoggedIn = DateTime.UtcNow;
await _userManager.UpdateAsync(user);
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Name, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
claims.AddRange(roles.Select(role => new Claim("role", role)));
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenAuthenticationOptions.SecretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
claims: claims,
expires: rememberMe ? DateTime.UtcNow.AddDays(7) : DateTime.UtcNow.AddHours(5),
signingCredentials: creds,
audience: "Ombi", issuer: "Ombi"
);
var accessToken = new JwtSecurityTokenHandler().WriteToken(token);
if (rememberMe)
{
// Save the token so we can refresh it later
//await _token.CreateToken(new Tokens() {Token = accessToken, User = user});
}
return new JsonResult(new
{
access_token = accessToken,
expiration = token.ValidTo
});
}
[HttpGet("{pinId:int}")]
public async Task<IActionResult> OAuth(int pinId)
{
var accessToken = await _plexOAuthManager.GetAccessTokenFromPin(pinId);
// Let's look for the users account
var account = await _plexOAuthManager.GetAccount(accessToken);
// Get the ombi user
var user = await _userManager.FindByNameAsync(account.user.username);
if (user == null)
{
// Could this be an email login?
user = await _userManager.FindByEmailAsync(account.user.email);
return new JsonResult(new
if (user == null)
{
access_token = accessToken,
expiration = token.ValidTo
});
return new UnauthorizedResult();
}
}
return new UnauthorizedResult();
return await CreateToken(true, user);
}
/// <summary>
@ -127,7 +184,7 @@ namespace Ombi.Controllers
{
return new UnauthorizedResult();
}
throw new NotImplementedException();
}

@ -6,5 +6,6 @@
public string Password { get; set; }
public bool RememberMe { get; set; }
public bool UsePlexAdminAccount { get; set; }
public bool UsePlexOAuth { get; set; }
}
}
Loading…
Cancel
Save