#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);
if (plexUser != null)
{
identity.UserName = plexUser.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;
}
}
}