#region Copyright // /************************************************************************ // Copyright (c) 2016 Jamie Rees // File: CustomAuthenticationProvider.cs // Created By: Jamie Rees // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion using System; using System.Linq; using Nancy; using Nancy.Authentication.Forms; using Nancy.Bootstrapper; using Nancy.Cookies; using Nancy.Cryptography; using Nancy.Extensions; using Nancy.Helpers; using Nancy.Security; using Ombi.Core; namespace Ombi.UI.Authentication { public class CustomAuthenticationProvider { private static string formsAuthenticationCookieName = "_ncfa"; private static CustomAuthenticationConfiguration currentConfiguration; /// Gets or sets the forms authentication cookie name public static string FormsAuthenticationCookieName { get { return CustomAuthenticationProvider.formsAuthenticationCookieName; } set { CustomAuthenticationProvider.formsAuthenticationCookieName = value; } } /// Enables forms authentication for the application /// Pipelines to add handlers to (usually "this") /// Forms authentication configuration public static void Enable(IPipelines pipelines, CustomAuthenticationConfiguration configuration) { if (pipelines == null) throw new ArgumentNullException("pipelines"); if (configuration == null) throw new ArgumentNullException("configuration"); if (!configuration.IsValid) throw new ArgumentException("Configuration is invalid", "configuration"); CustomAuthenticationProvider.currentConfiguration = configuration; pipelines.BeforeRequest.AddItemToStartOfPipeline(CustomAuthenticationProvider.GetLoadAuthenticationHook(configuration)); if (configuration.DisableRedirect) return; pipelines.AfterRequest.AddItemToEndOfPipeline(CustomAuthenticationProvider.GetRedirectToLoginHook(configuration)); } /// Enables forms authentication for a module /// Module to add handlers to (usually "this") /// Forms authentication configuration public static void Enable(INancyModule module, CustomAuthenticationConfiguration configuration) { if (module == null) throw new ArgumentNullException("module"); if (configuration == null) throw new ArgumentNullException("configuration"); if (!configuration.IsValid) throw new ArgumentException("Configuration is invalid", "configuration"); module.RequiresAuthentication(); CustomAuthenticationProvider.currentConfiguration = configuration; module.Before.AddItemToStartOfPipeline(CustomAuthenticationProvider.GetLoadAuthenticationHook(configuration)); if (configuration.DisableRedirect) return; module.After.AddItemToEndOfPipeline(CustomAuthenticationProvider.GetRedirectToLoginHook(configuration)); } /// /// Creates a response that sets the authentication cookie and redirects /// the user back to where they came from. /// /// Current context /// User identifier guid /// Optional expiry date for the cookie (for 'Remember me') /// Url to redirect to if none in the querystring /// Nancy response with redirect. public static Response UserLoggedInRedirectResponse(NancyContext context, Guid userIdentifier, DateTime? cookieExpiry = null, string fallbackRedirectUrl = null) { var redirectUrl = fallbackRedirectUrl; if (string.IsNullOrEmpty(redirectUrl)) { redirectUrl = context.Request.Url.BasePath; } if (string.IsNullOrEmpty(redirectUrl)) { redirectUrl = "/"; } string redirectQuerystringKey = GetRedirectQuerystringKey(currentConfiguration); if (context.Request.Query[redirectQuerystringKey].HasValue) { var queryUrl = (string)context.Request.Query[redirectQuerystringKey]; if (context.IsLocalUrl(queryUrl)) { redirectUrl = queryUrl; } } var response = context.GetRedirect(redirectUrl); var authenticationCookie = BuildCookie(userIdentifier, cookieExpiry, currentConfiguration); response.WithCookie(authenticationCookie); return response; } /// /// Logs the user in. /// /// User identifier guid /// Optional expiry date for the cookie (for 'Remember me') /// Nancy response with status public static Response UserLoggedInResponse(Guid userIdentifier, DateTime? cookieExpiry = null) { var response = (Response)HttpStatusCode.OK; var authenticationCookie = BuildCookie(userIdentifier, cookieExpiry, currentConfiguration); response.WithCookie(authenticationCookie); return response; } /// /// Logs the user out and redirects them to a URL /// /// Current context /// URL to redirect to /// Nancy response public static Response LogOutAndRedirectResponse(NancyContext context, string redirectUrl) { var response = context.GetRedirect(redirectUrl); var authenticationCookie = BuildLogoutCookie(currentConfiguration); response.WithCookie(authenticationCookie); return response; } /// /// Logs the user out. /// /// Nancy response public static Response LogOutResponse() { var response = (Response)HttpStatusCode.OK; var authenticationCookie = BuildLogoutCookie(currentConfiguration); response.WithCookie(authenticationCookie); return response; } /// /// Gets the pre request hook for loading the authenticated user's details /// from the cookie. /// /// Forms authentication configuration to use /// Pre request hook delegate private static Func GetLoadAuthenticationHook(CustomAuthenticationConfiguration configuration) { if (configuration == null) { throw new ArgumentNullException("configuration"); } return context => { var userGuid = GetAuthenticatedUserFromCookie(context, configuration); if (userGuid != Guid.Empty) { var identity = new UserIdentity(); var plexUsers = configuration.PlexUserRepository.GetAll(); var plexUser = plexUsers.FirstOrDefault(x => Guid.Parse(x.LoginId) == userGuid); var embyUsers = configuration.EmbyUserRepository.GetAll(); var embyUser = embyUsers.FirstOrDefault(x => Guid.Parse(x.LoginId) == userGuid); if (plexUser != null) { identity.UserName = plexUser.Username; } if (embyUser != null) { identity.UserName = embyUser.Username; } var localUsers = configuration.LocalUserRepository.GetAll(); var localUser = localUsers.FirstOrDefault(x => Guid.Parse(x.UserGuid) == userGuid); if (localUser != null) { identity.UserName = localUser.UserName; } context.CurrentUser = identity; } return null; }; } /// /// Gets the post request hook for redirecting to the login page /// /// Forms authentication configuration to use /// Post request hook delegate private static Action GetRedirectToLoginHook(CustomAuthenticationConfiguration configuration) { return context => { if (context.Response.StatusCode == HttpStatusCode.Unauthorized) { string redirectQuerystringKey = GetRedirectQuerystringKey(configuration); context.Response = context.GetRedirect( string.Format("{0}?{1}={2}", configuration.RedirectUrl, redirectQuerystringKey, context.ToFullPath("~" + context.Request.Path + HttpUtility.UrlEncode(context.Request.Url.Query)))); } }; } /// /// Gets the authenticated user GUID from the incoming request cookie if it exists /// and is valid. /// /// Current context /// Current configuration /// Returns user guid, or Guid.Empty if not present or invalid private static Guid GetAuthenticatedUserFromCookie(NancyContext context, CustomAuthenticationConfiguration configuration) { if (!context.Request.Cookies.ContainsKey(formsAuthenticationCookieName)) { return Guid.Empty; } var cookieValueEncrypted = context.Request.Cookies[formsAuthenticationCookieName]; if (string.IsNullOrEmpty(cookieValueEncrypted)) { return Guid.Empty; } var cookieValue = DecryptAndValidateAuthenticationCookie(cookieValueEncrypted, configuration); Guid returnGuid; if (string.IsNullOrEmpty(cookieValue) || !Guid.TryParse(cookieValue, out returnGuid)) { return Guid.Empty; } return returnGuid; } /// /// Build the forms authentication cookie /// /// Authenticated user identifier /// Optional expiry date for the cookie (for 'Remember me') /// Current configuration /// Nancy cookie instance private static INancyCookie BuildCookie(Guid userIdentifier, DateTime? cookieExpiry, CustomAuthenticationConfiguration configuration) { var cookieContents = EncryptAndSignCookie(userIdentifier.ToString(), configuration); var cookie = new NancyCookie(formsAuthenticationCookieName, cookieContents, true, configuration.RequiresSSL, cookieExpiry); if (!string.IsNullOrEmpty(configuration.Domain)) { cookie.Domain = configuration.Domain; } if (!string.IsNullOrEmpty(configuration.Path)) { cookie.Path = configuration.Path; } return cookie; } /// /// Builds a cookie for logging a user out /// /// Current configuration /// Nancy cookie instance private static INancyCookie BuildLogoutCookie(CustomAuthenticationConfiguration configuration) { var cookie = new NancyCookie(formsAuthenticationCookieName, String.Empty, true, configuration.RequiresSSL, DateTime.Now.AddDays(-1)); if (!string.IsNullOrEmpty(configuration.Domain)) { cookie.Domain = configuration.Domain; } if (!string.IsNullOrEmpty(configuration.Path)) { cookie.Path = configuration.Path; } return cookie; } /// /// Encrypt and sign the cookie contents /// /// Plain text cookie value /// Current configuration /// Encrypted and signed string private static string EncryptAndSignCookie(string cookieValue, CustomAuthenticationConfiguration configuration) { var encryptedCookie = configuration.CryptographyConfiguration.EncryptionProvider.Encrypt(cookieValue); var hmacBytes = GenerateHmac(encryptedCookie, configuration); var hmacString = Convert.ToBase64String(hmacBytes); return String.Format("{1}{0}", encryptedCookie, hmacString); } /// /// Generate a hmac for the encrypted cookie string /// /// Encrypted cookie string /// Current configuration /// Hmac byte array private static byte[] GenerateHmac(string encryptedCookie, CustomAuthenticationConfiguration configuration) { return configuration.CryptographyConfiguration.HmacProvider.GenerateHmac(encryptedCookie); } /// /// Decrypt and validate an encrypted and signed cookie value /// /// Encrypted and signed cookie value /// Current configuration /// Decrypted value, or empty on error or if failed validation public static string DecryptAndValidateAuthenticationCookie(string cookieValue, CustomAuthenticationConfiguration configuration) { var hmacStringLength = Base64Helpers.GetBase64Length(configuration.CryptographyConfiguration.HmacProvider.HmacLength); var encryptedCookie = cookieValue.Substring(hmacStringLength); var hmacString = cookieValue.Substring(0, hmacStringLength); var encryptionProvider = configuration.CryptographyConfiguration.EncryptionProvider; // Check the hmacs, but don't early exit if they don't match var hmacBytes = Convert.FromBase64String(hmacString); var newHmac = GenerateHmac(encryptedCookie, configuration); var hmacValid = HmacComparer.Compare(newHmac, hmacBytes, configuration.CryptographyConfiguration.HmacProvider.HmacLength); var decrypted = encryptionProvider.Decrypt(encryptedCookie); // Only return the decrypted result if the hmac was ok return hmacValid ? decrypted : string.Empty; } /// /// Gets the redirect query string key from /// /// The forms authentication configuration. /// Redirect Querystring key private static string GetRedirectQuerystringKey(CustomAuthenticationConfiguration configuration) { string redirectQuerystringKey = null; if (configuration != null) { redirectQuerystringKey = configuration.RedirectQuerystringKey; } if (string.IsNullOrWhiteSpace(redirectQuerystringKey)) { redirectQuerystringKey = CustomAuthenticationConfiguration.DefaultRedirectQuerystringKey; } return redirectQuerystringKey; } } }