Skip to content

Back Office OpenIdConnect authentication #16

@bhavens17

Description

@bhavens17

This isn't so much an issue as a question--do you have a working example of how to use an external OpenID Connect provider for back office authentication? I tried modifying your 14+ example for back office user authentication (see below), but after logging in to the external provider, it hits the 'OnTokenValidated' handler as expected but then I end up being redirected back to the standard back office login page. Am I missing something?

UmbracoBuilderExtensions.cs:

namespace Umbraco_OpenIdConnect_Example_v14plus.Extensions;

using System.Net;
using System.Security.Claims;
using Microsoft.Extensions.DependencyInjection;
using Provider;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Security;
using Umbraco.Extensions;

public static class UmbracoBuilderExtensions
{
    public static IUmbracoBuilder AddOpenIdConnectAuthentication(this IUmbracoBuilder builder)
    {
        builder.Services.ConfigureOptions<OpenIdConnectBackOfficeExternalLoginProviderOptions>();

        builder.AddBackOfficeExternalLogins(logins =>
        {
            logins.AddBackOfficeLogin(
                backOfficeAuthenticationBuilder =>
                {
                    backOfficeAuthenticationBuilder.AddOpenIdConnect(
						// The scheme must be set with this method to work for the umbraco members
						Constants.Security.BackOfficeExternalAuthenticationTypePrefix + OpenIdConnectBackOfficeExternalLoginProviderOptions.SchemeName,
                        options =>
                        {
                            var config = builder.Config;
                            options.ResponseType = "code";
                            options.Scope.Add("openid");
                            options.Scope.Add("profile");
                            options.Scope.Add("email");
                            options.Scope.Add("phone");
                            options.Scope.Add("address");
                            options.RequireHttpsMetadata = true;
                            options.MetadataAddress = config["OpenIdConnect:MetadataAddress"];
                            options.ClientId = config["OpenIdConnect:ClientId"];
                            // Normally the ClientSecret should not be in the Github repo.
                            // These settings are valid and only used for this example.
                            // So it's ok these are public.
                            options.ClientSecret = config["OpenIdConnect:ClientSecret"];
                            options.SaveTokens = true;
                            options.TokenValidationParameters.SaveSigninToken = true;
                            options.Events.OnTokenValidated = async context =>
                            {
                                var claims = context?.Principal?.Claims.ToList();
                                var email = claims?.SingleOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
                                if (email != null)
                                {
                                    // The email claim is required for auto linking.
                                    // So get it from another claim and put it in the email claim.
                                    claims?.Add(new Claim(ClaimTypes.Email, email.Value));
                                }

                                var name = claims?.SingleOrDefault(x => x.Type == "user_displayname");
                                if (name != null)
                                {
                                    // The name claim is required for auto linking.
                                    // So get it from another claim and put it in the name claim.
                                    claims?.Add(new Claim(ClaimTypes.Name, name.Value));
                                }
                                else
                                {
                                    name = claims?.SingleOrDefault(x => x.Type == "nickname");
                                    if (name != null)
                                    {
                                        // The name claim is required for auto linking.
                                        // So get it from another claim and put it in the name claim.
                                        claims?.Add(new Claim(ClaimTypes.Name, name.Value));
                                    }    
                                }

                                if (context != null)
                                {
                                    // Since we added new claims create a new principal.
                                    var authenticationType = context.Principal?.Identity?.AuthenticationType;
                                    context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType));
                                }

                                await Task.FromResult(0);
                            };
                            options.Events.OnRedirectToIdentityProviderForSignOut = async notification =>
                            {
                                var protocolMessage = notification.ProtocolMessage;

                                var logoutUrl = config["OpenIdConnect:LogoutUrl"];
                                var returnAfterLogout = config["OpenIdConnect:ReturnAfterLogout"];
                                if (!string.IsNullOrEmpty(logoutUrl) && !string.IsNullOrEmpty(returnAfterLogout))
                                {
                                    // Some external login providers require an IssuerAddress.
                                    // It requires the logout URL on the external login provider.
                                    // It also need the client_id and a URL which it needs to return to after logout.
                                    protocolMessage.IssuerAddress =
                                        $"{config["OpenIdConnect:LogoutUrl"]}" +
                                        $"?client_id={config["OpenIdConnect:ClientId"]}" +
                                        $"&returnTo={WebUtility.UrlEncode(config["OpenIdConnect:ReturnAfterLogout"])}";
                                }

                                // Since we're in a static extension method we need this approach to get the member manager. 
                                var memberManager = notification.HttpContext.RequestServices.GetService<IMemberManager>();
                                if (memberManager != null)
                                {
                                    var currentMember = await memberManager.GetCurrentMemberAsync();
                                    
                                    // On the current member we can find all their login tokens from the external login provider.
                                    // These tokens are stored in the umbracoExternalLoginToken table.
                                    var idToken = currentMember?.LoginTokens.FirstOrDefault(x => x.Name == "id_token");
                                    if (idToken != null && !string.IsNullOrEmpty(idToken.Value))
                                    {
                                        // Some external login providers need the IdTokenHint.
                                        // By setting the IdTokenHint the user can be redirected back from the external login provider to this website. 
                                        protocolMessage.IdTokenHint = idToken.Value;
                                    }
                                }

                                await Task.FromResult(0);
                            };
                        });
                });
        });
        return builder;
    }
}

OpenIdConnectMemberExternalLoginProviderOptions.cs (renamed to OpenIdConnectBackOfficeExternalLoginProviderOptions):

namespace Umbraco_OpenIdConnect_Example_v14plus.Provider;

using System.Collections.Generic;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Web.Common.Security;
using Umbraco.Cms.Core;
using Umbraco.Cms.Api.Management.Security;

public class OpenIdConnectBackOfficeExternalLoginProviderOptions : IConfigureNamedOptions<BackOfficeExternalLoginProviderOptions>
{
	public const string SchemeName = "OpenIdConnect";
	public void Configure(string? name, BackOfficeExternalLoginProviderOptions options)
	{
		if (name != Constants.Security.MemberExternalAuthenticationTypePrefix + SchemeName)
		{
			return;
		}

		Configure(options);
	}

	public void Configure(BackOfficeExternalLoginProviderOptions options)
	{
		options.AutoLinkOptions = new ExternalSignInAutoLinkOptions(
			// Must be true for auto-linking to be enabled
			autoLinkExternalAccount: true,

			// Optionally specify the default culture to create
			// the user as. If null it will use the default
			// culture defined in the web.config, or it can
			// be dynamically assigned in the OnAutoLinking
			// callback.
			defaultCulture: null

		// Optionally specify the default "IsApprove" status. Must be true for auto-linking.
		//defaultIsApproved: true,

		// Optionally specify the member type alias. Default is "Member"
		//defaultMemberTypeAlias: "Member",

		// Optionally specify the member groups names to add the auto-linking user to.
		//defaultMemberGroups: new List<string> { "example-group" }
		)
		{
			// Optional callback
			OnAutoLinking = (autoLinkUser, loginInfo) =>
			{
				// You can customize the user before it's linked.
				// i.e. Modify the user's groups based on the Claims returned
				// in the externalLogin info
				return;
			},
			OnExternalLogin = (user, loginInfo) =>
			{
				// You can customize the user before it's saved whenever they have
				// logged in with the external provider.
				// i.e. Sync the user's name based on the Claims returned
				// in the externalLogin info

				return true; //returns a boolean indicating if sign in should continue or not.
			}
		};
	}
}

App_Plugins/ExternalLoginProviders/umbraco-package.json (new, needed to add external provider button to back office login page):

{
  "$schema": "../../umbraco-package-schema.json",
  "name": "My Auth Package",
  "allowPublicAccess": true,
  "extensions": [
    {
      "type": "authProvider",
      "alias": "My.AuthProvider.Okta",
      "name": "My Okta Auth Provider",
      "forProviderName": "Umbraco.OpenIdConnect",
      "meta": {
        "label": "External Account",
        "defaultView": {
          "icon": "icon-cloud"
        },
        "behavior": {
          "autoRedirect": false
        },
        "linking": {
          "allowManualLinking": true
        }
      }
    }
  ]
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions