SSO in Umbraco 17: Azure AD Back Office Login with OpenID Connect
A step-by-step guide to wiring up Azure Active Directory as an external login provider for the Umbraco back office — with auto-linking…
SSO in Umbraco 17: Azure AD Back Office Login with OpenID Connect
A step-by-step guide to wiring up Azure Active Directory as an external login provider for the Umbraco back office — with auto-linking, group mapping, and graceful error handling.
Umbraco 17 ships with a refreshed back office built on the Umbraco Management API. That shift changes how external login providers are registered — and if you’ve copied examples from older versions, things will quietly break. This guide walks through a clean, production-ready integration with Azure Active Directory using OpenID Connect.
💡 All code shown is for Umbraco 17 (.NET 10). The
AddBackOfficeExternalLoginsAPI andBackOfficeExternalLoginProviderOptionsare specific to this version — do not use the deprecatedAddMemberor oldExternalLoginProviderspatterns.
What we’re building
By the end of this guide you will have:
- Azure AD configured as the identity provider
- A custom
IUmbracoBuilderextension that registers the OIDC provider - Auto-linking so Azure AD users map to existing Umbraco back-office accounts
- A front-end
umbraco-package.jsonextension so the SSO button appears on the login screen - Graceful error handling that redirects unknown users back to the login page with a readable message

Step 1 — Azure App Registration
In the Azure portal, create a new App Registration for your Umbraco site. Under Authentication, add a redirect URI pointing to your back office callback:
Azure Portal → App Registration → Authentication
https://your-domain.com/umbraco/signin-OpenIdConnect
Grant the following API permissions (Microsoft Graph, delegated): openid, profile, email. Generate a client secret and note down the Tenant ID, Client ID, and Client Secret.
Step 2 — Configuration in appsettings.json
Add an OpenIdConnect section to your appsettings.json. Keep secrets out of source control — use environment variables or Azure Key Vault in production.
appsettings.json
"OpenIdConnect": {
"TenantId": "your-tenant-id",
"ClientId": "your-client-id",
"ClientSecret": "your-client-secret",
"DisplayName": "Company Single Sign-On",
"DenyLocalLogin": "false",
"LogoutUrl": "https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/logout",
"ReturnAfterLogout": "https://your-domain.com/umbraco/login"
}
Step 3 — Constants file
Define scheme name and display name in one place so they stay consistent across the builder extension and the front-end package manifest.
Common/Helpers/Constants.cs
public static class CMSLoginProvider
{
// Must match forProviderName in umbraco-package.json
public const string SchemeName = "OpenIdConnect";
public const string DisplayName = "Company Single Sign-On";
}
⚠️ The SchemeName value here must exactly match the forProviderName field in umbraco-package.json, prefixed with Umbraco. — so OpenIdConnect becomes Umbraco.OpenIdConnect. Mismatches are a common silent failure.
Step 4 — The builder extension
Create a static extension class for IUmbracoBuilder. This is where the OIDC middleware is configured, claims are normalised, and auto-linking behaviour is defined.
Middlewares/UmbracoBuilderExtensions.cs
public static IUmbracoBuilder AddOpenIdConnectForBackoffice(
this IUmbracoBuilder builder)
{
builder.Services
.ConfigureOptions<OpenIdConnectBackOfficeExternalLoginProviderOptions>();
builder.AddBackOfficeExternalLogins(logins =>
{
logins.AddBackOfficeLogin(backofficeAuthBuilder =>
{
var config = builder.Config;
var schema = BackOfficeAuthenticationBuilder
.SchemeForBackOffice(
OpenIdConnectBackOfficeExternalLoginProviderOptions
.SchemeName);
var display = config["OpenIdConnect:DisplayName"]
?? CMSLoginProvider.DisplayName;
if (!string.IsNullOrEmpty(schema))
{
backofficeAuthBuilder.AddOpenIdConnect(
schema, display, options =>
{
options.ResponseType = "code";
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Authority = $"https://login.microsoftonline.com/
{config["OpenIdConnect:TenantId"]}/v2.0";
options.ClientId = config["OpenIdConnect:ClientId"];
options.ClientSecret = config["OpenIdConnect:ClientSecret"];
options.SaveTokens = true;
options.TokenValidationParameters.NameClaimType
= ClaimTypes.Name;
options.TokenValidationParameters.RoleClaimType
= ClaimTypes.Role;
options.SignInScheme
= Constants.Security.BackOfficeExternalAuthenticationType;
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = context =>
{
// Normalise the email claim across Azure AD token formats
var email =
context.Principal?.FindFirstValue("email")
?? context.Principal?.FindFirstValue("upn")
?? context.Principal?.FindFirstValue(
"preferred_username");
if (string.IsNullOrEmpty(email))
return Task.CompletedTask;
// Reject users not already in Umbraco
var userService = context.HttpContext
.RequestServices
.GetRequiredService<IUserService>();
if (userService.GetByEmail(email) == null)
{
context.Response.Cookies.Append(
"UmbracoBackofficeLoginError",
"Unauthorized access",
new CookieOptions {
Path = "/umbraco/login",
HttpOnly = false,
Secure = true,
SameSite = SameSiteMode.Lax,
Expires = DateTimeOffset.Now.AddMinutes(1)
});
context.Response.Redirect("/umbraco/login");
context.HandleResponse();
return Task.CompletedTask;
}
// Rebuild the principal with normalised claims
var claims = context.Principal!.Claims.ToList();
claims.Add(new Claim(ClaimTypes.Email, email));
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(
claims,
context.Principal.Identity?.AuthenticationType));
return Task.CompletedTask;
},
OnRedirectToIdentityProviderForSignOut = context =>
{
var logoutUrl = config["OpenIdConnect:LogoutUrl"];
var postLogout = config["OpenIdConnect:ReturnAfterLogout"];
if (!string.IsNullOrEmpty(logoutUrl))
context.ProtocolMessage.IssuerAddress =
$"{logoutUrl}?client_id=" +
$"{config["OpenIdConnect:ClientId"]}" +
$"&returnTo={WebUtility.UrlEncode(postLogout)}";
return Task.CompletedTask;
}
};
});
}
});
});
return builder;
}
Step 5 — Auto-link options
The IConfigureNamedOptions implementation controls how external users map to Umbraco accounts. Set autoLinkExternalAccount: true so a matching Umbraco user is found automatically on first login — no manual linking step required.
Middlewares/UmbracoBuilderExtensions.cs (continued)
public class OpenIdConnectBackOfficeExternalLoginProviderOptions
: IConfigureNamedOptions<BackOfficeExternalLoginProviderOptions>
{
public const string SchemeName = CMSLoginProvider.SchemeName;
private readonly IConfiguration _config;
public OpenIdConnectBackOfficeExternalLoginProviderOptions(
IConfiguration config) => _config = config;
public void Configure(
string? name,
BackOfficeExternalLoginProviderOptions options)
{
if (name != BackOfficeAuthenticationBuilder
.SchemeForBackOffice(SchemeName)) return;
Configure(options);
}
public void Configure(
BackOfficeExternalLoginProviderOptions options)
{
options.AutoLinkOptions = new ExternalSignInAutoLinkOptions(
autoLinkExternalAccount: true,
defaultUserGroups: Array.Empty<string>(),
defaultCulture: null,
allowManualLinking: true)
{
OnAutoLinking = (user, _) => { user.IsApproved = true; },
OnExternalLogin = (_, _) => true
};
bool.TryParse(
_config["OpenIdConnect:DenyLocalLogin"],
out bool denyLocalLogin);
options.DenyLocalLogin = denyLocalLogin;
}
}
💡 Set defaultUserGroups to a specific Umbraco group alias (e.g. new[] { "editor" }) if you want newly auto-linked users to receive a default role automatically.
Step 6 — Register in Program.cs
Chain the extension method directly into the Umbraco builder pipeline:
Program.cs
builder.CreateUmbracoBuilder()
.AddBackOffice()
.AddWebsite()
.AddComposers()
.AddOpenIdConnectForBackoffice() // ← add this
.Build();
Step 7 — Front-end package manifest
The new Umbraco 17 back office requires a package extension to surface the SSO button on the login screen. Create an umbraco-package.json in your package folder. The forProviderName must be Umbraco. followed by your scheme name exactly.
App_Plugins/MyAuthPackage/umbraco-package.json
{
"$schema": "../../umbraco-package-schema.json",
"name": "My Auth Package",
"allowPublicAccess": true,
"weight": 999,
"extensions": [
{
"type": "authProvider",
"alias": "Umbraco.OpenIdConnect",
"name": "My Auth Provider",
"forProviderName": "Umbraco.OpenIdConnect",
"meta": {
"label": "Company Single Sign-On",
"defaultView": {
"icon": "icon-cloud"
},
"behavior": {
"autoRedirect": false
},
"linking": {
"allowManualLinking": true
}
}
}
]
} 메타데이터
- post_id
- 549a0e5f91e5
- slug
- sso-in-umbraco-17-azure-ad-back-office-login-with-openid-connect-549a0e5f91e5
- url
- https://medium.com/@nomansiddiquins1/sso-in-umbraco-17-azure-ad-back-office-login-with-openid-connect-549a0e5f91e5
- canonical_url
- https://medium.com/@nomansiddiquins1/sso-in-umbraco-17-azure-ad-back-office-login-with-openid-connect-549a0e5f91e5
- author_url
- https://medium.com/@nomansiddiquins1
- status
- ok
- fetched_at
- 2026-06-13 12:55:53