diff --git a/src/Ombi.Api.Plex/IPlexApi.cs b/src/Ombi.Api.Plex/IPlexApi.cs index 0734262cf..cc61dfa5d 100644 --- a/src/Ombi.Api.Plex/IPlexApi.cs +++ b/src/Ombi.Api.Plex/IPlexApi.cs @@ -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 GetUsers(string authToken); Task GetAccount(string authToken); Task GetRecentlyAdded(string authToken, string uri, string sectionId); + Task CreatePin(); + Task GetPin(int pinId); + Uri GetOAuthUrl(int pinId, string code, string applicationUrl, bool wizard); } } \ No newline at end of file diff --git a/src/Ombi.Api.Plex/Models/OAuth/OAuthPin.cs b/src/Ombi.Api.Plex/Models/OAuth/OAuthPin.cs new file mode 100644 index 000000000..e65cd91d4 --- /dev/null +++ b/src/Ombi.Api.Plex/Models/OAuth/OAuthPin.cs @@ -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; } + } + +} \ No newline at end of file diff --git a/src/Ombi.Api.Plex/PlexApi.cs b/src/Ombi.Api.Plex/PlexApi.cs index e6c52d1df..5fb87aca7 100644 --- a/src/Ombi.Api.Plex/PlexApi.cs +++ b/src/Ombi.Api.Plex/PlexApi.cs @@ -1,20 +1,49 @@ -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; namespace Ombi.Api.Plex { public class PlexApi : IPlexApi { - public PlexApi(IApi api) + public PlexApi(IApi api, ISettingsService settings) { Api = api; + _plex = settings; } private IApi Api { get; } + private readonly ISettingsService _plex; + + private string _clientId; + private string ClientIdSecret + { + get + { + if (string.IsNullOrEmpty(_clientId)) + { + var settings = _plex.GetSettings(); + if (settings.UniqueInstallCode.IsNullOrEmpty()) + { + settings.UniqueInstallCode = Guid.NewGuid().ToString("N"); + _plex.SaveSettings(settings); + } + + _clientId = settings.UniqueInstallCode; + } + + return _clientId; + } + } private const string SignInUri = "https://plex.tv/users/sign_in.json"; private const string FriendsUri = "https://plex.tv/pms/friends/all"; @@ -156,6 +185,50 @@ namespace Ombi.Api.Plex return await Api.Request(request); } + public async Task CreatePin() + { + var request = new Request($"api/v2/pins", "https://plex.tv/", HttpMethod.Post); + request.AddQueryString("strong", "true"); + AddHeaders(request); + + return await Api.Request(request); + } + + public async Task GetPin(int pinId) + { + var request = new Request($"api/v2/pins/{pinId}", "https://plex.tv/", HttpMethod.Get); + AddHeaders(request); + + return await Api.Request(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($"api/v1/PlexOAuth/{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{ClientIdSecret}"); + + 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; + } + /// /// Adds the required headers and also the authorization header /// @@ -173,7 +246,7 @@ namespace Ombi.Api.Plex /// private void AddHeaders(Request request) { - request.AddHeader("X-Plex-Client-Identifier", $"OmbiV3"); + request.AddHeader("X-Plex-Client-Identifier", $"OmbiV3{ClientIdSecret}"); request.AddHeader("X-Plex-Product", "Ombi"); request.AddHeader("X-Plex-Version", "3"); request.AddContentHeader("Content-Type", request.ContentType == ContentType.Json ? "application/json" : "application/xml"); diff --git a/src/Ombi.Api/Request.cs b/src/Ombi.Api/Request.cs index e4120ed9c..89c3a7f2d 100644 --- a/src/Ombi.Api/Request.cs +++ b/src/Ombi.Api/Request.cs @@ -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; } diff --git a/src/Ombi.Core/Authentication/PlexOAuthManager.cs b/src/Ombi.Core/Authentication/PlexOAuthManager.cs new file mode 100644 index 000000000..d3bab0a05 --- /dev/null +++ b/src/Ombi.Core/Authentication/PlexOAuthManager.cs @@ -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 settings) + { + _api = api; + _customizationSettingsService = settings; + } + + private readonly IPlexApi _api; + private readonly ISettingsService _customizationSettingsService; + + public async Task RequestPin() + { + var pin = await _api.CreatePin(); + return pin; + } + + public async Task 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 GetAccount(string accessToken) + { + return await _api.GetAccount(accessToken); + } + + public async Task 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; + } + } +} \ No newline at end of file diff --git a/src/Ombi.Core/IPlexOAuthManager.cs b/src/Ombi.Core/IPlexOAuthManager.cs new file mode 100644 index 000000000..142d4162a --- /dev/null +++ b/src/Ombi.Core/IPlexOAuthManager.cs @@ -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 GetAccessTokenFromPin(int pinId); + Task RequestPin(); + Task GetOAuthUrl(int pinId, string code); + Uri GetWizardOAuthUrl(int pinId, string code, string websiteAddress); + Task GetAccount(string accessToken); + } +} \ No newline at end of file diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index e2f667465..68f4b7218 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -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(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } public static void RegisterHttp(this IServiceCollection services) { diff --git a/src/Ombi.Settings/Settings/Models/External/PlexSettings.cs b/src/Ombi.Settings/Settings/Models/External/PlexSettings.cs index 3fcde951a..dd92eba18 100644 --- a/src/Ombi.Settings/Settings/Models/External/PlexSettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/PlexSettings.cs @@ -6,8 +6,8 @@ namespace Ombi.Core.Settings.Models.External public sealed class PlexSettings : Ombi.Settings.Settings.Models.Settings { public bool Enable { get; set; } + public string UniqueInstallCode { get; set; } public List Servers { get; set; } - } public class PlexServers : ExternalSettings diff --git a/src/Ombi/ClientApp/app/auth/auth.service.ts b/src/Ombi/ClientApp/app/auth/auth.service.ts index b9899c9a4..92b41ccd9 100644 --- a/src/Ombi/ClientApp/app/auth/auth.service.ts +++ b/src/Ombi/ClientApp/app/auth/auth.service.ts @@ -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 { + return this.http.get(`${this.url}/${pin}`, {headers: this.headers}); + } + public requiresPassword(login: IUserLogin): Observable { return this.http.post(`${this.url}/requirePassword`, JSON.stringify(login), {headers: this.headers}); } diff --git a/src/Ombi/ClientApp/app/interfaces/IPlex.ts b/src/Ombi/ClientApp/app/interfaces/IPlex.ts index 7125ae4bc..823b80d32 100644 --- a/src/Ombi/ClientApp/app/interfaces/IPlex.ts +++ b/src/Ombi/ClientApp/app/interfaces/IPlex.ts @@ -2,6 +2,10 @@ user: IPlexUser; } +export interface IPlexOAuthAccessToken { + accessToken: string; +} + export interface IPlexUser { email: string; uuid: string; diff --git a/src/Ombi/ClientApp/app/services/applications/index.ts b/src/Ombi/ClientApp/app/services/applications/index.ts index 9433dfce0..98c61cf04 100644 --- a/src/Ombi/ClientApp/app/services/applications/index.ts +++ b/src/Ombi/ClientApp/app/services/applications/index.ts @@ -4,3 +4,4 @@ export * from "./plex.service"; export * from "./radarr.service"; export * from "./sonarr.service"; export * from "./tester.service"; +export * from "./plexoauth.service"; diff --git a/src/Ombi/ClientApp/app/services/applications/plex.service.ts b/src/Ombi/ClientApp/app/services/applications/plex.service.ts index c04a990e1..53fd31f9d 100644 --- a/src/Ombi/ClientApp/app/services/applications/plex.service.ts +++ b/src/Ombi/ClientApp/app/services/applications/plex.service.ts @@ -29,4 +29,8 @@ export class PlexService extends ServiceHelpers { public getFriends(): Observable { return this.http.get(`${this.url}Friends`, {headers: this.headers}); } + + public oAuth(wizard: boolean): Observable { + return this.http.get(`${this.url}oauth/${wizard}`, {headers: this.headers}); + } } diff --git a/src/Ombi/ClientApp/app/services/applications/plexoauth.service.ts b/src/Ombi/ClientApp/app/services/applications/plexoauth.service.ts new file mode 100644 index 000000000..59a884714 --- /dev/null +++ b/src/Ombi/ClientApp/app/services/applications/plexoauth.service.ts @@ -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 { + return this.http.get(`${this.url}${pin}`, {headers: this.headers}); + } +} diff --git a/src/Ombi/ClientApp/app/wizard/plex/plex.component.html b/src/Ombi/ClientApp/app/wizard/plex/plex.component.html index 2ba63c43e..6c4b846a7 100644 --- a/src/Ombi/ClientApp/app/wizard/plex/plex.component.html +++ b/src/Ombi/ClientApp/app/wizard/plex/plex.component.html @@ -20,6 +20,13 @@ + +
+
+ +
+
+ diff --git a/src/Ombi/ClientApp/app/wizard/plex/plex.component.ts b/src/Ombi/ClientApp/app/wizard/plex/plex.component.ts index 146d794fe..6bc21d628 100644 --- a/src/Ombi/ClientApp/app/wizard/plex/plex.component.ts +++ b/src/Ombi/ClientApp/app/wizard/plex/plex.component.ts @@ -70,4 +70,12 @@ export class PlexComponent { }); }); } + + public oauth() { + this.plexService.oAuth(true).subscribe(x => { + if(x.url) { + window.location.href = x.url; + } + }); + } } diff --git a/src/Ombi/ClientApp/app/wizard/plex/plexoauth.component.html b/src/Ombi/ClientApp/app/wizard/plex/plexoauth.component.html new file mode 100644 index 000000000..d60f1d1f0 --- /dev/null +++ b/src/Ombi/ClientApp/app/wizard/plex/plexoauth.component.html @@ -0,0 +1,14 @@ + + +
+
+
+

Plex Authentication

+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/src/Ombi/ClientApp/app/wizard/plex/plexoauth.component.ts b/src/Ombi/ClientApp/app/wizard/plex/plexoauth.component.ts new file mode 100644 index 000000000..1397fe7f5 --- /dev/null +++ b/src/Ombi/ClientApp/app/wizard/plex/plexoauth.component.ts @@ -0,0 +1,76 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; + +import { ConfirmationService } from "primeng/primeng"; + +import { PlexOAuthService, IdentityService, 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 confirmationService: ConfirmationService, + private identityService: IdentityService, + private settings: SettingsService, + private router: Router, + private auth: AuthService) { + + this.route.params + .subscribe((params: any) => { + this.pinId = params.pin; + }); + } + + ngOnInit(): void { + this.plexOauth.oAuth(this.pinId).subscribe(x => { + x.accessToken; + + 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({ + username: "", + password: "", + usePlexAdminAccount: true, + }).subscribe(x => { + if (x) { + 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(x => { + 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; + } + }); + }, + reject: () => { + this.router.navigate(["Wizard/CreateAdmin"]); + }, + }); + }); + } + +} diff --git a/src/Ombi/ClientApp/app/wizard/wizard.module.ts b/src/Ombi/ClientApp/app/wizard/wizard.module.ts index 96cbdddc1..7eae2e4f4 100644 --- a/src/Ombi/ClientApp/app/wizard/wizard.module.ts +++ b/src/Ombi/ClientApp/app/wizard/wizard.module.ts @@ -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, ], }) diff --git a/src/Ombi/Controllers/External/PlexController.cs b/src/Ombi/Controllers/External/PlexController.cs index 2d45d7565..cde78bc64 100644 --- a/src/Ombi/Controllers/External/PlexController.cs +++ b/src/Ombi/Controllers/External/PlexController.cs @@ -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, - ILogger logger) + ILogger logger, IPlexOAuthManager manager) { PlexApi = plexApi; PlexSettings = plexSettings; _log = logger; + _plexOAuthManager = manager; } private IPlexApi PlexApi { get; } private ISettingsService PlexSettings { get; } private readonly ILogger _log; + private readonly IPlexOAuthManager _plexOAuthManager; /// /// Signs into the Plex API. @@ -66,6 +69,7 @@ namespace Ombi.Controllers.External _log.LogDebug("Adding first server"); settings.Enable = true; + settings.UniqueInstallCode = Guid.NewGuid().ToString("N"); settings.Servers = new List { new PlexServers { @@ -173,5 +177,37 @@ namespace Ombi.Controllers.External // Filter out any dupes return vm.DistinctBy(x => x.Id); } + + [HttpGet("oauth/{wizard:bool}")] + [AllowAnonymous] + public async Task 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()}); + } } } diff --git a/src/Ombi/Controllers/PlexOAuthController.cs b/src/Ombi/Controllers/PlexOAuthController.cs index 8f33e37be..2aad2a2a9 100644 --- a/src/Ombi/Controllers/PlexOAuthController.cs +++ b/src/Ombi/Controllers/PlexOAuthController.cs @@ -1,9 +1,19 @@ -using System.Net; +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 { @@ -12,29 +22,57 @@ namespace Ombi.Controllers [AllowAnonymous] public class PlexOAuthController : Controller { - - [HttpGet] - public IActionResult OAuthCallBack() + public PlexOAuthController(IPlexOAuthManager manager, IPlexApi plexApi, ISettingsService plexSettings, + ILogger log) { - var bodyStr = ""; - var req = Request; + _manager = manager; + _plexApi = plexApi; + _plexSettings = plexSettings; + _log = log; + } - // Allows using several time the stream in ASP.Net Core - req.EnableRewind(); + private readonly IPlexOAuthManager _manager; + private readonly IPlexApi _plexApi; + private readonly ISettingsService _plexSettings; + private readonly ILogger _log; - // Arguments: Stream, Encoding, detect encoding, buffer size - // AND, the most important: keep stream opened - using (StreamReader reader - = new StreamReader(req.Body, Encoding.UTF8, true, 1024, true)) + [HttpGet("{pinId:int}")] + public async Task 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) { - bodyStr = reader.ReadToEnd(); + _log.LogWarning("Looks like we can't find any Plex Servers"); } + _log.LogDebug("Adding first server"); - // Rewind, so the core is not lost when it looks the body for the request - req.Body.Position = 0; + settings.Enable = true; + settings.Servers = new List { + 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", + } + }; - // Do your work with bodyStr - return Ok(); + await _plexSettings.SaveSettingsAsync(settings); + return Json(new { accessToken }); } } } diff --git a/src/Ombi/Controllers/SettingsController.cs b/src/Ombi/Controllers/SettingsController.cs index 30469cd57..867a7517c 100644 --- a/src/Ombi/Controllers/SettingsController.cs +++ b/src/Ombi/Controllers/SettingsController.cs @@ -142,7 +142,13 @@ namespace Ombi.Controllers [HttpGet("plex")] public async Task PlexSettings() { - return await Get(); + var s = await Get(); + if (s.UniqueInstallCode.IsNullOrEmpty()) + { + s.UniqueInstallCode = Guid.NewGuid().ToString("N"); + } + + return s; } /// diff --git a/src/Ombi/Controllers/TokenController.cs b/src/Ombi/Controllers/TokenController.cs index 5d524b7ba..62e43635d 100644 --- a/src/Ombi/Controllers/TokenController.cs +++ b/src/Ombi/Controllers/TokenController.cs @@ -25,18 +25,21 @@ namespace Ombi.Controllers [Produces("application/json")] public class TokenController { - public TokenController(OmbiUserManager um, IOptions ta, IAuditRepository audit, ITokenRepository token) + public TokenController(OmbiUserManager um, IOptions 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; /// /// Gets the token. @@ -69,63 +72,99 @@ namespace Ombi.Controllers // Verify Password if (await _userManager.CheckPasswordAsync(user, model.Password)) { - 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 - { - 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) - { - // Save the token so we can refresh it later - //await _token.CreateToken(new Tokens() {Token = accessToken, User = user}); - } + return await CreateToken(model.RememberMe, user); + } + } + 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) + { return new JsonResult(new { - access_token = accessToken, - expiration = token.ValidTo + error = "Application URL has not been set" }); } + return new RedirectResult(url.ToString()); } - else + + return new UnauthorizedResult(); + } + + private async Task CreateToken(bool rememberMe, OmbiUser user) + { + var roles = await _userManager.GetRolesAsync(user); + + if (roles.Contains(OmbiRoles.Disabled)) { - // Plex OAuth - // Redirect them to Plex + return new UnauthorizedResult(); + } - var request = new Request("auth", "https://app.plex.tv", HttpMethod.Get); - request.AddQueryString("clientID", "OMBIv3"); - request.AddQueryString("forwardUrl", "http://localhost:5000"); - request.AddQueryString("context-device-product", "http://localhost:5000"); - return new RedirectResult("https://app.plex.tv/auth#?forwardUrl=http://localhost:5000/api/v1/plexoauth&clientID=OMBIv3&context%5Bdevice%5D%5Bproduct%5D=Ombi%20SSO"); + user.LastLoggedIn = DateTime.UtcNow; + await _userManager.UpdateAsync(user); + var claims = new List + { + 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 UnauthorizedResult(); + return new JsonResult(new + { + access_token = accessToken, + expiration = token.ValidTo + }); + } + + [HttpGet("{pinId:int}")] + public async Task 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); + + if (user == null) + { + return new UnauthorizedResult(); + } + } + + return await CreateToken(true, user); } ///