Search code examples
c#.netazure-active-directoryopenid-connectumbraco

How do I get Azure Active Directory App Roles to show in Claims?


I'm using Azure Active Directory to provide authentication to the Backoffice on my website running Umbraco version 11.0.

This is working nicely and I can log in but I want to improve the experience by using app roles within Azure to manage the user's group within Umbraco.

My Azure setup

I've created an App Registration within Azure with the following configuration:

  • Added a Redirection URI:
  • URI: https://localhost:44391/umbraco-signin-microsoft/
  • Enabled Access tokens (used for implicit flows)
  • Enabled ID tokens (used for implicit and hybrid flows)
  • Supported account types: Accounts in this organizational directory only (Example only - Single tenant)
  • Added App Roles
    • Administrator
    • Editor

enter image description here

In Enterprise Applications, I've also added the App Roles above to my users:

enter image description here

My code

Login Provider

namespace Example.Api.Features.Authentication.Extensions;

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

        builder.AddBackOfficeExternalLogins(logins =>
        {
            const string schema = MicrosoftAccountDefaults.AuthenticationScheme;
                
            logins.AddBackOfficeLogin(
                backOfficeAuthenticationBuilder =>
                {
                    backOfficeAuthenticationBuilder.AddMicrosoftAccount(
                        // the scheme must be set with this method to work for the back office
                        backOfficeAuthenticationBuilder.SchemeForBackOffice(OpenIdConnectBackOfficeExternalLoginProviderOptions.SchemeName) ?? string.Empty,
                        options =>
                        {
                            //By default this is '/signin-microsoft' but it needs to be changed to this
                            options.CallbackPath = "/umbraco-signin-microsoft/";
                            //Obtained from the AZURE AD B2C WEB APP
                            options.ClientId = "CLIENT_ID";
                            //Obtained from the AZURE AD B2C WEB APP
                            options.ClientSecret = "CLIENT_SECRET";
                            options.TokenEndpoint = $"https://login.microsoftonline.com/TENANT/oauth2/v2.0/token";
                            options.AuthorizationEndpoint = $"https://login.microsoftonline.com/TENANT/oauth2/v2.0/authorize";
                        });
                });
        });

        return builder;
    }
}

Auto-linking accounts

namespace Example.Api.Features.Configuration;

public class OpenIdConnectBackOfficeExternalLoginProviderOptions : IConfigureNamedOptions<BackOfficeExternalLoginProviderOptions>
{
    public const string SchemeName = "OpenIdConnect";

    public void Configure(string name, BackOfficeExternalLoginProviderOptions options)
    {
        if (name != "Umbraco." + 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 default user group, else
            // assign in the OnAutoLinking callback
            // (default is editor)
            defaultUserGroups: new[] { Constants.Security.EditorGroupAlias },

            // Optionally you can disable the ability to link/unlink
            // manually from within the back office. Set this to false
            // if you don't want the user to unlink from this external
            // provider.
            allowManualLinking: false
        )
        {
            // 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

                autoLinkUser.IsApproved = true;
            },
            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.
            }
        };

        // Optionally you can disable the ability for users
        // to login with a username/password. If this is set
        // to true, it will disable username/password login
        // even if there are other external login providers installed.
        options.DenyLocalLogin = true;

        // Optionally choose to automatically redirect to the
        // external login provider so the user doesn't have
        // to click the login button. This is
        options.AutoRedirectLoginToExternalProvider = true;
    }
}

In this file, I'd ideally do as the comment says and i.e. Modify the user's groups based on the Claims returned in the externalLogin info.

Also registered in my Startup file

services.AddUmbraco(_env, _config)
            .AddBackOffice()
            .AddWebsite()
            .AddComposers()
            .ConfigureAuthentication()
            .Build();

I've attempted to give the following permissions to the application, with no luck:

enter image description here

Current state of play is that I can login just fine but if I debug externalInfo, there's nothing in there about the users having either the Administrator or Editor App Role as configured above.

My gut feeling is that I'm missing something with the Azure Active Directory setup but I've tried a few different configurations and can't seem to get the App Roles to come back.

Thanks,

Ben

EDIT - 15.02.2023:

I can see that the roles come back when I hit the https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token endpoint using client_credentials as the grant_type. It looks like the .NET application using authorization_code instead. I've decoded the token retrieved from this and it doesn't contain the roles.

I wonder if there's some kind of configuration on the .NET application that allows me to add the roles.


Solution

  • To solve this, I ended up swapping out the AddMicrosoftAccount AuthenticationBuilder in favour of AddOpenIdConnect. This appears to respect the claims in the tokens.

    This is the code I am now using in the ConfigureAuthentication method.

    public static IUmbracoBuilder ConfigureAuthentication(this IUmbracoBuilder builder)
    {
        // Register OpenIdConnectBackOfficeExternalLoginProviderOptions here rather than require it in startup
        builder.Services.ConfigureOptions<OpenIdConnectBackOfficeExternalLoginProviderOptions>();
    
        builder.AddBackOfficeExternalLogins(logins =>
        {
            logins.AddBackOfficeLogin(
                backOfficeAuthenticationBuilder =>
                {
                    backOfficeAuthenticationBuilder.AddOpenIdConnect(
                        // The scheme must be set with this method to work for the back office
                        backOfficeAuthenticationBuilder.SchemeForBackOffice(OpenIdConnectBackOfficeExternalLoginProviderOptions.SchemeName),
                        options =>
                        {
                            options.CallbackPath = "/umbraco-signin-microsoft/";
                            // use cookies
                            options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                            // pass configured options along
                            options.Authority = "https://login.microsoftonline.com/{tenantId}/v2.0";
                            options.ClientId = "{clientId}";
                            options.ClientSecret = "{clientSecret}";
                            // Use the authorization code flow
                            options.ResponseType = OpenIdConnectResponseType.Code;
                            options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
                            // map claims
                            options.TokenValidationParameters.NameClaimType = "name";
                            options.TokenValidationParameters.RoleClaimType = "role";
    
                            options.RequireHttpsMetadata = true;
                            options.GetClaimsFromUserInfoEndpoint = true;
                            options.SaveTokens = true;
                            options.UsePkce = true;
                            
                            options.Scope.Add("email");
                        });
                });
        });
        return builder;
    }