SaaS CMS has officially launched! Learn more now.

User login with IdentityServer and `AddOpenIdConnect`

Vote:
 

Recently I migrated our solution from CMS11 to CMS12. 

For the login, we have used our own IdentityServer IDP, which provides all the claims required for the logged in user. 

Following is the code i have added to the startup, 

 public void ConfigureServices(IServiceCollection services)
        {
            services.AddHttpContextAccessor();
            services.AddLogging();
            services.AddMvc();
            services.ConfigureViewEngine();

            services.AddCommunityServices();
            services.AddCms()
                    .AddFind()
                    .AddCmsAspNetIdentity<ApplicationUser>();
            services.AddCommunityOpenIdConnect(_configuration);
            services.AddDistributedMemoryCache();
            services.AddSession(options =>
            {
                options.IdleTimeout = TimeSpan.FromMinutes(10);
                options.Cookie.HttpOnly = true;
                options.Cookie.IsEssential = true;
            });
            services.AddTinyMceConfiguration();

            if (!_webHostingEnvironment.IsDevelopment())
            {
                services.AddCmsCloudPlatformSupport(_configuration);
            }

}
public static IServiceCollection AddCommunityOpenIdConnect(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
                options.DefaultAuthenticateScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie(options =>
            {
                var communityAuthService = services.BuildServiceProvider().GetRequiredService<ICommunityAuthService>();
                options.Events.OnSignedIn = context => OnSignedIn(context, communityAuthService);
            })
            .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
            {
                ConfigureOpenIdConnectOptions(configuration, services, "CommunityIDP", options);
            });
            return services;
        }

        private static void ConfigureOpenIdConnectOptions(IConfiguration configuration, IServiceCollection services, string section, OpenIdConnectOptions options)
        {
            configuration.Bind(section, options);
            options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.ClientId = "";           
            options.ResponseType = OpenIdConnectResponseType.CodeIdTokenToken;
            options.SaveTokens = true;
            options.Scope.Add(Constants.IDENTITY_SCOPE);
            options.GetClaimsFromUserInfoEndpoint = true;
            options.ClaimActions.MapUniqueJsonKey("person_id", "person_id");
            options.ClaimActions.MapUniqueJsonKey("contact_id", "contact_id");
            options.ClaimActions.MapUniqueJsonKey("so_employee", "so_employee");
            options.ClaimActions.MapUniqueJsonKey("company_category", "company_category");
            options.ClaimActions.MapUniqueJsonKey("email", "email");
            options.ClaimActions.MapUniqueJsonKey("associate_name", "associate_name");
            options.ClaimActions.Add(new RoleClaimAction());
            options.Events.OnRedirectToIdentityProvider = context => OnRedirectToIdentityProvider(context, configuration);
            options.Events.OnTicketReceived = OnTicketReceived;
        }
           

        private static Task OnTicketReceived(TicketReceivedContext context)
        {
            var identity = context.Principal.Identity as ClaimsIdentity;
            if (identity != null)
            {
                identity.AddClaim(new Claim(ClaimTypes.Role, "Everyone"));
            }

            return Task.CompletedTask;
        }


        private static async Task OnSignedIn(CookieSignedInContext context, ICommunityAuthService communityAuthService)
        {
            //Sync user and the roles to EPiServer in the background
            List<string> additionalClaimsToSync = new() { ServiceConstants.Claims.PersonID, ServiceConstants.Claims.Email };

            _ = ServiceLocator.Current.GetInstance<ISynchronizingUserService>()
                .SynchronizeAsync(context.Principal.Identity as ClaimsIdentity, additionalClaimsToSync);

            //int newCommunityUserId = await communityAuthService.PostAuthenticationTasksAsync(context.Principal);
          
        }

        private static Task OnAuthenticationFailed(AuthenticationFailedContext context)
        {           
            context.Response.Redirect("/error/AccessDenied");
            context.HandleResponse();
            return Task.CompletedTask;
        }

        private static Task OnRedirectToIdentityProvider(RedirectContext context, IConfiguration configuration)
        {

            var homePageUri = configuration.GetValue<string>("CommunityIDP:HomePageUri");

            if (context.Properties.Items.TryGetValue(Constants.LOGIN_HINT, out var loginHintValue))
            {
                context.ProtocolMessage.LoginHint = loginHintValue.ToString();
            }
            else
            {
                // Redirect user to home with redirectUrl when the user tries to access a restricted page when not logged in
                if (context.Response.StatusCode == StatusCodes.Status401Unauthorized &&
                 context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication &&
                 context.Request != null && !context.HttpContext.User.Identity.IsAuthenticated)
                {
                    context.HandleResponse();
                    context.Response.Redirect($"{homePageUri}?{Constants.QUERYSTRING_RETURNURL}={context.Request.Path}{context.Request.QueryString}");
                }
            }
          
            return Task.CompletedTask;
        }

I have mapped the custom claims separately, and also used a role claim action to map the role claims that is received as an array from IdentityServer. (It happens when there are more than one claim type returned) 


The issue is, even when I call the ISynchronizingUserService, all the required claims are available after the transformation. 

However, AddCookie does not seems to create te correct identity cookie and the session. 

It does create some cookie, but it does not allow me to login. 

In the `StartPageController` , `Login` action, i have called the `Challenge` method. 

 return Challenge(properties, OpenIdConnectDefaults.AuthenticationScheme);

And the usual happens, shows the IDP login page, once verified, returns back to the OIDC handler, maps the claims and the rest. 

What am I missing,  why the user does not login ? 

Do I have to manually call the HttpContext,SigninAsync ? 

#305618
Edited, Jul 24, 2023 6:15
Vote:
 

Hi Huzaim

If the application creates a cookie like ".AspNetCore.Identity.Application", then the user is logged in. But maybe not in the way the authorization middleware expects it.

Does it work if you remove "options.DefaultAuthenticateScheme = OpenIdConnectDefaults.AuthenticationScheme;"?

#305633
Jul 24, 2023 16:29
Huzaim - Jul 25, 2023 2:12
The name of the cookie that gets created is ".AspNetCore.Cookies".
Then, I deliberately gave a custom name for it `options.Cookie.Name="SampleCookieName"`, just to verify that it is the `AddCookie` handler that creates the cookie. And it is in fact the AddCookie handler.

Removing "options.DefaultAuthenticateScheme = OpenIdConnectDefaults.AuthenticationScheme;" did not work.

Also, I just created an admin user using https://github.com/episerver/netcore-preview/blob/master/Quicksilver/EPiServer.Reference.Commerce.Site/Infrastructure/UsersInstaller.cs, and I was able to then login using `util/login` and the ".AspNetCore.Identity.Application" cookie gets created as well.

It's just, I cannot login from the front end using the claims returned via the OIDC.

Vote:
 

Try changing the .AddAuthentication section to this:

services.AddAuthentication(options =>
{
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})

And the beginning of the .AddOpenIdConnect section to this:

.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>

Another thing. In your OnSignedIn handler you don't await the synchronization of claims. This could potentially be an issue.

#305789
Jul 27, 2023 16:36
Vote:
 

I recently did a blog post on implementing ID4 with Optimizely, some of the content in their may be of use to you? https://world.optimizely.com/blogs/allthingsopti/dates/2023/2/a-day-in-the-life-of-an-optimizely-developer---implementing-identity-server-4-and-asp-net-identity/ 

#305848
Jul 28, 2023 14:17
Huzaim - Aug 02, 2023 8:44
Thanks, it was very helpful.
Vote:
 

I was reading through the following blog https://swapcode.wordpress.com/2018/09/24/using-openid-connect-with-episerver/, and it suggested to remove the code related to identity since we are using `ISynchronizingUserService`. 

Removed this line, 

 .AddCmsAspNetIdentity<ApplicationUser>();

and the code worked fine with the OIDC. 

#306092
Edited, Aug 02, 2023 8:48
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.