Allows the ability to use external logins to login to authorize upgrades, this means being able to add reserved paths at startup dynamically which is now built in as part of the AuthenticationOptionsExtensions for registering external logins for the back office.

This commit is contained in:
Shannon
2015-04-02 14:46:53 +11:00
parent 22ac571c7d
commit a321d4d1b8
8 changed files with 177 additions and 72 deletions

View File

@@ -51,7 +51,15 @@
<umb-notifications></umb-notifications>
@Html.BareMinimumServerVariables(Url, (string)ViewBag.UmbracoPath)
@{
var externalLoginUrl = Url.Action("ExternalLogin", "BackOffice", new
{
area = ViewBag.UmbracoPath,
//Custom redirect URL since we don't want to just redirect to the back office since this is for authing upgrades
redirectUrl = Url.Action("AuthorizeUpgrade", "BackOffice")
});
}
@Html.BareMinimumServerVariables(Url, externalLoginUrl)
@Html.AngularExternalLoginInfoValues((IEnumerable<string>)ViewBag.ExternalSignInError)

View File

@@ -66,7 +66,7 @@
<umb-notifications></umb-notifications>
@Html.BareMinimumServerVariables(Url, (string)ViewBag.UmbracoPath)
@Html.BareMinimumServerVariables(Url, Url.Action("ExternalLogin", "BackOffice", new { area = ViewBag.UmbracoPath }))
@Html.AngularExternalLoginInfoValues((IEnumerable<string>)ViewBag.ExternalSignInError)

View File

@@ -63,27 +63,9 @@ namespace Umbraco.Web.Editors
/// <returns></returns>
public async Task<ActionResult> Default()
{
ViewBag.UmbracoPath = GlobalSettings.UmbracoMvcArea;
//check if there's errors in the TempData, assign to view bag and render the view
if (TempData["ExternalSignInError"] != null)
{
ViewBag.ExternalSignInError = TempData["ExternalSignInError"];
return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml");
}
//First check if there's external login info, if there's not proceed as normal
var loginInfo = await OwinContext.Authentication.GetExternalLoginInfoAsync(
Core.Constants.Security.BackOfficeExternalAuthenticationType);
if (loginInfo == null)
{
return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml");
}
//we're just logging in with an external source, not linking accounts
return await ExternalSignInAsync(loginInfo);
return await RenderDefaultOrProcessExternalLoginAsync(
() => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"),
() => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"));
}
/// <summary>
@@ -92,11 +74,13 @@ namespace Umbraco.Web.Editors
/// </summary>
/// <returns></returns>
[HttpGet]
public ActionResult AuthorizeUpgrade()
public async Task<ActionResult> AuthorizeUpgrade()
{
ViewBag.UmbracoPath = GlobalSettings.UmbracoMvcArea;
return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/AuthorizeUpgrade.cshtml");
return await RenderDefaultOrProcessExternalLoginAsync(
//The default view to render when there is no external login info or errors
() => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/AuthorizeUpgrade.cshtml"),
//The ActionResult to perform if external login is successful
() => Redirect("/"));
}
/// <summary>
@@ -422,11 +406,15 @@ namespace Umbraco.Web.Editors
}
[HttpPost]
public ActionResult ExternalLogin(string provider)
public ActionResult ExternalLogin(string provider, string redirectUrl = null)
{
if (redirectUrl == null)
{
redirectUrl = Url.Action("Default", "BackOffice");
}
// Request a redirect to the external login provider
return new ChallengeResult(provider,
Url.Action("Default", "BackOffice"));
return new ChallengeResult(provider, redirectUrl);
}
[UmbracoAuthorize]
@@ -466,9 +454,42 @@ namespace Umbraco.Web.Editors
return RedirectToLocal(Url.Action("Default", "BackOffice"));
}
private async Task<ActionResult> ExternalSignInAsync(ExternalLoginInfo loginInfo)
/// <summary>
/// Used by Default and AuthorizeUpgrade to render as per normal if there's no external login info, otherwise
/// process the external login info.
/// </summary>
/// <returns></returns>
private async Task<ActionResult> RenderDefaultOrProcessExternalLoginAsync(Func<ActionResult> defaultResponse, Func<ActionResult> externalSignInResponse)
{
if (defaultResponse == null) throw new ArgumentNullException("defaultResponse");
if (externalSignInResponse == null) throw new ArgumentNullException("externalSignInResponse");
ViewBag.UmbracoPath = GlobalSettings.UmbracoMvcArea;
//check if there's errors in the TempData, assign to view bag and render the view
if (TempData["ExternalSignInError"] != null)
{
ViewBag.ExternalSignInError = TempData["ExternalSignInError"];
return defaultResponse();
}
//First check if there's external login info, if there's not proceed as normal
var loginInfo = await OwinContext.Authentication.GetExternalLoginInfoAsync(
Core.Constants.Security.BackOfficeExternalAuthenticationType);
if (loginInfo == null)
{
return defaultResponse();
}
//we're just logging in with an external source, not linking accounts
return await ExternalSignInAsync(loginInfo, externalSignInResponse);
}
private async Task<ActionResult> ExternalSignInAsync(ExternalLoginInfo loginInfo, Func<ActionResult> response)
{
if (loginInfo == null) throw new ArgumentNullException("loginInfo");
if (response == null) throw new ArgumentNullException("response");
// Sign in the user with this external login provider if the user already has a login
var user = await UserManager.FindAsync(loginInfo.Login);
@@ -493,8 +514,8 @@ namespace Umbraco.Web.Editors
Response.Cookies[Core.Constants.Security.BackOfficeExternalCookieName].Expires = DateTime.MinValue;
}
}
return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml");
return response();
}
private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent)

View File

@@ -19,13 +19,16 @@ namespace Umbraco.Web
/// </summary>
/// <param name="html"></param>
/// <param name="uri"></param>
/// <param name="umbracoPath"></param>
/// <param name="externalLoginsUrl">
/// The post url used to sign in with external logins - this can change depending on for what service the external login is service.
/// Example: normal back office login or authenticating upgrade login
/// </param>
/// <returns></returns>
/// <remarks>
/// These are the bare minimal server variables that are required for the application to start without being authenticated,
/// we will load the rest of the server vars after the user is authenticated.
/// </remarks>
public static IHtmlString BareMinimumServerVariables(this HtmlHelper html, UrlHelper uri, string umbracoPath)
public static IHtmlString BareMinimumServerVariables(this HtmlHelper html, UrlHelper uri, string externalLoginsUrl)
{
var str = @"<script type=""text/javascript"">
var Umbraco = {};
@@ -34,7 +37,7 @@ namespace Umbraco.Web
""umbracoUrls"": {
""authenticationApiBaseUrl"": """ + uri.GetUmbracoApiServiceBaseUrl<AuthenticationController>(controller => controller.PostLogin(null)) + @""",
""serverVarsJs"": """ + uri.GetUrlWithCacheBust("ServerVariables", "BackOffice") + @""",
""externalLoginsUrl"": """ + uri.Action("ExternalLogin", "BackOffice", new {area = umbracoPath}) + @"""
""externalLoginsUrl"": """ + externalLoginsUrl + @"""
},
""application"": {
""applicationPath"": """ + html.ViewContext.HttpContext.Request.ApplicationPath + @"""

View File

@@ -1,31 +0,0 @@
using Microsoft.Owin.Security;
using Umbraco.Core;
namespace Umbraco.Web.Security.Identity
{
public static class AuthenticationDescriptionOptionsExtensions
{
/// <summary>
/// Configures the properties of the authentication description instance for use with Umbraco back office
/// </summary>
/// <param name="options"></param>
/// <param name="style"></param>
/// <param name="icon"></param>
public static void ForUmbracoBackOffice(this AuthenticationOptions options, string style, string icon)
{
Mandate.ParameterNotNullOrEmpty(options.AuthenticationType, "options.AuthenticationType");
//Ensure the prefix is set
if (options.AuthenticationType.StartsWith(Constants.Security.BackOfficeExternalAuthenticationTypePrefix) == false)
{
options.AuthenticationType = Constants.Security.BackOfficeExternalAuthenticationTypePrefix + options.AuthenticationType;
}
options.Description.Properties["SocialStyle"] = style;
options.Description.Properties["SocialIcon"] = icon;
//flag for use in back office
options.Description.Properties["UmbracoBackOffice"] = true;
}
}
}

View File

@@ -0,0 +1,67 @@
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Umbraco.Core;
using Umbraco.Core.Logging;
namespace Umbraco.Web.Security.Identity
{
public static class AuthenticationOptionsExtensions
{
/// <summary>
/// Configures the properties of the authentication description instance for use with Umbraco back office
/// </summary>
/// <param name="options"></param>
/// <param name="style"></param>
/// <param name="icon"></param>
/// <param name="callbackPath">
/// This is important if the identity provider is to be able to authenticate when upgrading Umbraco. We will try to extract this from
/// any options passed in via reflection since none of the default OWIN providers inherit from a base class but so far all of them have a consistent
/// name for the 'CallbackPath' property which is of type PathString. So we'll try to extract it if it's not found or supplied.
///
/// If a value is extracted or supplied, this will be added to an internal list which the UmbracoModule will use to allow the request to pass
/// through without redirecting to the installer.
/// </param>
public static void ForUmbracoBackOffice(this AuthenticationOptions options, string style, string icon, string callbackPath = null)
{
Mandate.ParameterNotNullOrEmpty(options.AuthenticationType, "options.AuthenticationType");
//Ensure the prefix is set
if (options.AuthenticationType.StartsWith(Constants.Security.BackOfficeExternalAuthenticationTypePrefix) == false)
{
options.AuthenticationType = Constants.Security.BackOfficeExternalAuthenticationTypePrefix + options.AuthenticationType;
}
options.Description.Properties["SocialStyle"] = style;
options.Description.Properties["SocialIcon"] = icon;
//flag for use in back office
options.Description.Properties["UmbracoBackOffice"] = true;
if (callbackPath.IsNullOrWhiteSpace())
{
try
{
//try to get it with reflection
var prop = options.GetType().GetProperty("CallbackPath");
if (prop != null && TypeHelper.IsTypeAssignableFrom<PathString>(prop.PropertyType))
{
//get the value
var path = (PathString) prop.GetValue(options);
if (path.HasValue)
{
UmbracoModule.ReservedPaths.TryAdd(path.ToString());
}
}
}
catch (System.Exception ex)
{
LogHelper.Error(typeof (AuthenticationOptionsExtensions), "Could not read AuthenticationOptions properties", ex);
}
}
else
{
UmbracoModule.ReservedPaths.TryAdd(callbackPath);
}
}
}
}

View File

@@ -558,7 +558,7 @@
<Compile Include="Scheduling\ILatchedBackgroundTask.cs" />
<Compile Include="Scheduling\RecurringTaskBase.cs" />
<Compile Include="Security\Identity\AppBuilderExtensions.cs" />
<Compile Include="Security\Identity\AuthenticationDescriptionOptionsExtensions.cs" />
<Compile Include="Security\Identity\AuthenticationOptionsExtensions.cs" />
<Compile Include="Security\Identity\AuthenticationManagerExtensions.cs" />
<Compile Include="Security\Identity\BackOfficeCookieManager.cs" />
<Compile Include="Security\Identity\FormsAuthenticationSecureDataFormat.cs" />

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -31,8 +32,7 @@ namespace Umbraco.Web
// Request.RawUrl is still there
// response.Redirect does?! always remap to /vdir?!
public class
UmbracoModule : IHttpModule
public class UmbracoModule : IHttpModule
{
#region HttpModule event handlers
@@ -277,7 +277,7 @@ namespace Umbraco.Web
/// <param name="httpContext"></param>
/// <param name="uri"></param>
/// <returns></returns>
static bool EnsureDocumentRequest(HttpContextBase httpContext, Uri uri)
bool EnsureDocumentRequest(HttpContextBase httpContext, Uri uri)
{
var maybeDoc = true;
var lpath = uri.AbsolutePath.ToLowerInvariant();
@@ -311,7 +311,7 @@ namespace Umbraco.Web
// at that point, either we have no extension, or it is .aspx
// if the path is reserved then it cannot be a document request
if (maybeDoc && GlobalSettings.IsReservedPathOrUrl(lpath, httpContext, RouteTable.Routes))
if (maybeDoc && GlobalSettings.IsReservedPathOrUrl(lpath, httpContext, _combinedRouteCollection.Value))
maybeDoc = false;
//NOTE: No need to warn, plus if we do we should log the document, as this message doesn't really tell us anything :)
@@ -612,5 +612,42 @@ namespace Umbraco.Web
EndRequest(this, args);
}
#endregion
/// <summary>
/// This is used to be passed into the GlobalSettings.IsReservedPathOrUrl and will include some 'fake' routes
/// used to determine if a path is reserved.
/// </summary>
/// <remarks>
/// This is basically used to reserve paths dynamically
/// </remarks>
private readonly Lazy<RouteCollection> _combinedRouteCollection = new Lazy<RouteCollection>(() =>
{
var allRoutes = new RouteCollection();
foreach (var route in RouteTable.Routes)
{
allRoutes.Add(route);
}
foreach (var reservedPath in ReservedPaths)
{
try
{
allRoutes.Add("_umbreserved_" + reservedPath.ReplaceNonAlphanumericChars(""),
new Route(reservedPath.TrimStart('/'), new StopRoutingHandler()));
}
catch (Exception ex)
{
LogHelper.Error<UmbracoModule>("Could not add reserved path route", ex);
}
}
return allRoutes;
});
/// <summary>
/// This is used internally to track any registered callback paths for Identity providers. If the request path matches
/// any of the registered paths, then the module will let the request keep executing
/// </summary>
internal static readonly ConcurrentHashSet<string> ReservedPaths = new ConcurrentHashSet<string>();
}
}