← Back to list

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…

Noman Siddiqui · 2026-05-29 18:47 · 1 claps · 3.7 min read
#umbraco #dotnet #sso #openid-connect #oauth2
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud 🎬 · Film & Television

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 AddBackOfficeExternalLogins API and BackOfficeExternalLoginProviderOptions are specific to this version — do not use the deprecated AddMember or old ExternalLoginProviders patterns.

What we’re building

By the end of this guide you will have:

  • Azure AD configured as the identity provider
  • A custom IUmbracoBuilder extension that registers the OIDC provider
  • Auto-linking so Azure AD users map to existing Umbraco back-office accounts
  • A front-end umbraco-package.json extension 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