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
}
}
}
]
}
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:
OpenIdConnectMemberExternalLoginProviderOptions.cs (renamed to OpenIdConnectBackOfficeExternalLoginProviderOptions):
App_Plugins/ExternalLoginProviders/umbraco-package.json (new, needed to add external provider button to back office login page):