Search code examples
c#identityserver4umbraco8

Umbraco 8 backoffice login with IdentityServer4


Background

I'm quite new to Umbraco but have been trying to use IdentityServer4 for the BackOffice of Umbraco. For the IDP, I've used the in-memory configuration (is4inmem template) found here.

For Umbraco I've used the UmbracoIdentityExtensions to configure OpenId Connect.

I've been mainly following this tutorial (this is however, for Umbraco 7).

The problem

I do have the 'Sign in with OpenId connect' button which I configured, but when I try to log in using the IDP, Umbraco does not log me in. I keep getting returned to the login page. Whenever I go to the IDP page, however, I am logged in and can see I've given access as seen in the picture below. enter image description here

Whenever I log in with an Umbraco account, and then try to 'Link your OpenId Connect account', it does nothing, but upon logging out an error message appears in the screen: 'An error occurred, could not get external login info' I've tried to use different configuration settings, but without success.

Code

IDP Config.cs

public static IEnumerable<IdentityResource> Ids =>
            new IdentityResource[]
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Email(),
                new IdentityResource(
                    name: "application.profile",
                    displayName: "Application profile",
                    claimTypes: new[] { ClaimTypes.GivenName, ClaimTypes.Surname }
                )
            };

... etc ...

 public static IEnumerable<Client> Clients =>
            new Client[]
            {
                new Client
                {
                    ClientId = "u-client-bo",
                    ClientSecrets = new List<Secret>
                    {
                        new Secret("secret".Sha256()),
                    },
                    ClientName = "Umbraco Client",
                    AllowedGrantTypes = GrantTypes.Hybrid,
                    RequireConsent = false,
                    RedirectUris           = { "https://localhost:44302/Umbraco" },
                    PostLogoutRedirectUris = { "https://localhost:44302/Umbraco" },
                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.Email,
                        "application.profile",
                    },
                    AllowAccessTokensViaBrowser = true,
                    AlwaysIncludeUserClaimsInIdToken = false
                }
            };

For Umbraco, I have edited the UmbracoCustomOwinStartup to the following:

public class UmbracoCustomOwinStartup : UmbracoDefaultOwinStartup
{
    protected override void ConfigureUmbracoUserManager(IAppBuilder app)
    {
        app.ConfigureUserManagerForUmbracoBackOffice(
            Services,
            Mapper,
            UmbracoSettings.Content,
            GlobalSettings,

            global::Umbraco.Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider());
    }

    protected override void ConfigureUmbracoAuthentication(IAppBuilder app)
    {
        app
            .UseUmbracoBackOfficeCookieAuthentication(UmbracoContextAccessor, RuntimeState, Services.UserService, GlobalSettings, UmbracoSettings.Security, PipelineStage.Authenticate)
            .UseUmbracoBackOfficeExternalCookieAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, PipelineStage.Authenticate)
            .UseUmbracoPreviewAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, UmbracoSettings.Security, PipelineStage.Authorize);

        var identityOptions = new OpenIdConnectAuthenticationOptions
        {
            ClientId = "u-client-bo",
            SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie,
            Authority = "https://localhost:44393",
            RedirectUri = "https://localhost:44302/Umbraco",
            ResponseType = "code id_token token",
            Scope = "openid profile application.profile",
            PostLogoutRedirectUri = "https://localhost:44302/Umbraco",

            Notifications = new OpenIdConnectAuthenticationNotifications
            {
                SecurityTokenValidated = ClaimsTransformer.GenerateUserIdentityAsync
            }
        };

        // Configure BackOffice Account Link button and style
        identityOptions.ForUmbracoBackOffice("btn-microsoft", "fa-windows");
        identityOptions.Caption = "OpenId Connect";

        // Configure AutoLinking
        identityOptions.SetExternalSignInAutoLinkOptions(
            new ExternalSignInAutoLinkOptions(autoLinkExternalAccount: true));

        app.UseOpenIdConnectAuthentication(identityOptions);
    }
}

The ClaimsTransformer.GenerateUserIdentityAsync method adds additional claims to the Identity.

Am I missing additional configuration or components?

Thanks in advance!


Solution

  • I've finally figured it out. There were several issues:

    1. Correct the auth cookie

    Instead of using DefaultAuthenticationTypes.ExternalCookie as SignInAsAuthenticationType , I am now using Umbraco.Core.Constants.Security.BackOfficeExternalAuthenticationType.

    2. Set the AuthenticationType

    Set the AuthenticationType in the OpenIdConnectAuthenticationOptions. It must match the name of the Authority in order for auto-link to work.

    Important: Set it again after identityOptions.ForUmbracoBackOffice("btn-microsoft", "fa-windows"); explicitly since it prefixes it with 'Umbraco.' after the method call.

    3. Include the email claim

    I've added the email claim, this is also required for auto-link to work.

    Scope = "openid email profile application.profile",

    4. Ensure that you have any form of name claim

    I've set AlwaysIncludeUserClaimsInIdToken to true in the IDP, so the id claims get automatically in Umbraco. My ClaimsTransformer looks like this now:

    public class ClaimsTransformer
    {
        public static async Task GenerateUserIdentityAsync(
            SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
        {
            // Now this contains ID claims (e.g. GivenName in my case)
            var id = notification.AuthenticationTicket.Identity;
    
            var identityUser = new ClaimsIdentity(
                id.Claims, // copy the claims I have
                notification.AuthenticationTicket.Identity.AuthenticationType,
                // set the nameType, so Umbraco can use the 'ExternalLogin.Name' for auto-link to work
                ClaimTypes.GivenName, // <-- You have to set a correct nameType claim
                ClaimTypes.Role);
    
             notification.AuthenticationTicket = new AuthenticationTicket(identityUser,
                    notification.AuthenticationTicket.Properties);
        }
    }
    

    5. Remove other Umbraco Auth middleware

    The app.UseUmbracoBackOfficeXXX statements were not needed in my case, in fact, they broke the functionality. My UmbracoCustomOwinStartup looks like this now:

    public class UmbracoCustomOwinStartup : UmbracoDefaultOwinStartup
    {
        protected override void ConfigureUmbracoAuthentication(IAppBuilder app)
        {
            base.ConfigureUmbracoAuthentication(app);
    
            var identityOptions = new OpenIdConnectAuthenticationOptions
            {
                ClientId = "u-client-bo",
                SignInAsAuthenticationType = Umbraco.Core.Constants.Security.BackOfficeExternalAuthenticationType,
                AuthenticationType = "https://localhost:44393",
                Authority = "https://localhost:44393",
                RedirectUri = "https://localhost:44302/Umbraco",
                ResponseType = "code id_token token",
                Scope = "openid email profile application.profile",
                PostLogoutRedirectUri = "https://localhost:44302/Umbraco",
    
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    SecurityTokenValidated = ClaimsTransformer.GenerateUserIdentityAsync
                }
            };
    
            // Configure BackOffice Account Link button and style
            identityOptions.ForUmbracoBackOffice("btn-microsoft", "fa-windows");
            identityOptions.Caption = "OpenId Connect";
    
            identityOptions.AuthenticationType = "https://localhost:44393";
    
            // Configure AutoLinking
            identityOptions.SetExternalSignInAutoLinkOptions(
                new ExternalSignInAutoLinkOptions(autoLinkExternalAccount: true));
    
            app.UseOpenIdConnectAuthentication(identityOptions);
        }
    
    }
    

    tip: Don't forget to use the correct OWIN Startup in your web.config.

    I hope some of you found this helpful, I couldn't find alot of documentation about Umbraco 8 in combination with IdentityServer4.