November Happy Hour will be moved to Thursday December 5th.

CMS 8 - Federated Security with OpenIdConnect

Vote:
 

We have a asp.net webforms project where we use Episerver as CMS 8. Recently, we switched to federated security with Owin Middleware as described here (but instead of WSFederation, we use OpenIdConnect)

With old configuration, we didn't have an application level authorisation rule. But after switching to federated security, we realised that while first login/logout happens successfully, second login attempt gets stuck in a redirection loop due to absence of "OpenIdConnect.nonce" cookie. In order to create the cookie again, we had to add authorisation rule to deny any unauthorised access. But now we cannot access public pages without authentication.

My questions are:
1- What is the right configuration for federated security with OpenIdConnect in Episerver?
2- Has anyone had the same "second login redirection loop" issue before? If so, how did they solve it?
2- How should the authorisation rules be for public pages and application in general?
3- If we have to add an authorisation rule to allow access to public pages, how can we add this for multi-language pages?

Note: Our authentication point is always Default.aspx, we don't have separate login/logout pages.

Owin Configuration

 public void Configuration(IAppBuilder app)
        {
            app.Use((context, next) =>
            {
                var httpContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
                httpContext.SetSessionStateBehavior(SessionStateBehavior.Required);
                return next();
            }).UseStageMarker(PipelineStage.MapHandler);

            // This is used to prevent an issue, which leads to the following exception when trying to login to CMS:
            // IDX10311: RequireNonce is 'true' (default) but validationContext.Nonce is null.
            app.UseKentorOwinCookieSaver();

            app.SetDefaultSignInAsAuthenticationType(SsoSettings.AuthenticationType);
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = SsoSettings.AuthenticationType //".AspNet.OpenIdConnect-Login"
            });
            
            var clientId = SsoSettings.ClientId;
            var clientSecret = SsoSettings.ClientSecret;
            var authority = SsoSettings.Authority;
            var redirectUri = SsoSettings.RedirectUri;

            app.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    AuthenticationType = SsoSettings.AuthenticationType,
                    ClientId = clientId,
                    ClientSecret = SsoSettings.ClientSecret,
                    Authority = authority,
                    RedirectUri = redirectUri,
                    PostLogoutRedirectUri = redirectUri,
                    ResponseType = SsoSettings.ResponseType,
                    Scope = SsoSettings.Scope,
                    UseTokenLifetime = true,
                    Notifications = new OpenIdConnectAuthenticationNotifications
                    {
                        AuthorizationCodeReceived = async n =>
                        {
                            var tokenClient = new TokenClient($"{authority}/connect/token", clientId, clientSecret);
                            var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, redirectUri)
                                .ConfigureAwait((false));
                            if (tokenResponse.IsError)
                                throw new ArgumentException(tokenResponse.Error);

                            var userInfoClient = new UserInfoClient(
                                new Uri($"{authority}/connect/userinfo"),
                                tokenResponse.AccessToken);

                            var userInfoResponse = await userInfoClient.GetAsync().ConfigureAwait(false);
                            var claims = userInfoResponse.GetClaimsIdentity().Claims.ToList();

                            n.AuthenticationTicket.Properties.Dictionary.Add(SsoSettings.IdTokenKey,
                                tokenResponse.IdentityToken);
                            n.AuthenticationTicket = new AuthenticationTicket(
                                new ClaimsIdentity(claims, SsoSettings.AuthenticationType, JwtClaimTypes.Email,
                                    JwtClaimTypes.Role), n.AuthenticationTicket.Properties);

                            //Sync user and the roles to EPiServer in the background
                            await ServiceLocator.Current.GetInstance<SynchronizingUserService>()
                                .SynchronizeAsync(n.AuthenticationTicket.Identity).ConfigureAwait(false);

                        },
                        RedirectToIdentityProvider = n =>
                        {
                            // Check if we have set a variable in Owin to prompt for login
                            var promptForLogin = n.OwinContext.Get<string>(SsoSettings.PromptKey) ==
                                                 SsoSettings.PromptLoginKey;
                            if (promptForLogin)
                            {
                                // Set the prompt
                                n.ProtocolMessage.Prompt = SsoSettings.PromptLoginKey;

                                // If we are going to logout, when the user is authenticated and the token is not expired
                                if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
                                {
                                    // Cancel it, because it should prompt for login
                                    n.ProtocolMessage.RequestType = OpenIdConnectRequestType.AuthenticationRequest;
                                    n.HandleResponse();

                                    // Challenge again, so it will try to Sign in again
                                    n.OwinContext.Authentication.Challenge(new AuthenticationProperties
                                    {
                                        RedirectUri = SsoSettings.RedirectUri,
                                    }, SsoSettings.AuthenticationType);
                                }
                            }

                            // If we are going to logout, but the user is not authenticated or the token is not valid
                            if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
                            {
                                // Try to send IdToken, so that the post_logout_redirect_uri is enabled
                                var idTokenHint = n.OwinContext.Get<string>(SsoSettings.IdTokenKey);
                                if (idTokenHint != null)
                                {
                                    n.ProtocolMessage.IdTokenHint = idTokenHint;
                                }
                            }

                            return Task.FromResult(0);
                        },
                        SecurityTokenValidated = ctx =>
                        {
                            //Ignore scheme/host name in redirect Uri to make sure a redirect to HTTPS does not redirect back to HTTP
                            var redirect = new Uri(ctx.AuthenticationTicket.Properties.RedirectUri, UriKind.RelativeOrAbsolute);

                            if (redirect.IsAbsoluteUri)
                                ctx.AuthenticationTicket.Properties.RedirectUri = redirect.PathAndQuery;
                            return Task.FromResult(0);
                        },
                        //Nonce Validation Error after claering all cookies and loggin in again:
                        //IDX10311: RequireNonce is 'true' (default) but validationContext.Nonce is null. A nonce cannot be validated. If you don't need to check the nonce, set OpenIdConnectProtocolValidator.RequireNonce to 'false'.
                        //workaound found here: https://github.com/IdentityServer/IdentityServer3/issues/931#issuecomment-237958480
                        AuthenticationFailed = (context) =>
                        {
                            if (context.Exception.Message.StartsWith("OICE_20004") ||
                                context.Exception.Message.Contains("IDX10311"))
                            {
                                context.SkipToNextMiddleware();
                                return Task.FromResult(0);
                            }

                            return Task.FromResult(0);
                        }
                    }
                });

            app.Map("/util/logout.aspx", map =>
            {
                map.Run(ctx =>
                {
                    var currentHost = ctx.Request.Uri.GetLeftPart(UriPartial.Authority);
                    var returnUrl = $"{currentHost}/maintenance";

                    AuthenticationHelper.Logout(returnUrl);
                    return Task.FromResult(0);
                });
            });

            //Add stage marker to make sure OpenID Connect runs on Authenticate (before URL Authorization and virtual roles)
            app.UseStageMarker(PipelineStage.Authenticate);

            //Tell antiforgery to use the name claim
            AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
        }

Web.config

<configuration>
  <system.web>
	<authentication mode="None">
    <profile enabled="true" defaultProvider="SqlProfile" automaticSaveEnabled="true">
      <properties>
        <add name="Address" type="System.String" />
        ...
      </properties>
      <providers>
        <clear />
        <add name="SqlProfile" type="System.Web.Profile.SqlProfileProvider, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="EPiServerDB" applicationName="EPiServerSample" />
      </providers>
    </profile>
    <membership>
      <providers>
        <clear />
      </providers>
    </membership>
    <roleManager enabled="false">
      <providers>
        <clear />
      </providers>
    </roleManager>
    <authorization>
      <allow roles="CRMUsers, WebEditors, WebAdmins" />
      <deny users="*" />
    </authorization>
  </system.web>
  <episerver.framework>
    <virtualRoles addClaims="true">
      <providers>
        <add name="Administrators" type="EPiServer.Security.WindowsAdministratorsRole, EPiServer.Framework" />
        <add name="Everyone" type="EPiServer.Security.EveryoneRole, EPiServer.Framework" />
        <add name="Authenticated" type="EPiServer.Security.AuthenticatedRole, EPiServer.Framework" />
        <add name="Anonymous" type="EPiServer.Security.AnonymousRole, EPiServer.Framework" />
        <add name="CmsAdmins" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="WebAdmins, Administrators, CustomerPortalWebAdmins" mode="Any" />
        <add name="WebAdmins" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="WebAdmins, Administrators, CustomerPortalWebAdmins" mode="Any" />
        <add name="CmsEditors" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="WebEditors, CustomerPortalWebAdmins" mode="Any" />
        <add name="Creator" type="EPiServer.Security.CreatorRole, EPiServer" />
        <add name="PackagingAdmins" type="EPiServer.Security.MappedRole, EPiServer.Framework" roles="WebAdmins, Administrators, CustomerPortalWebAdmins" mode="Any" />
      </providers>
    </virtualRoles>
	...
    <securityEntity>
      <providers>
        <add name="SynchronizingProvider" type="EPiServer.Security.SynchronizingRolesSecurityEntityProvider, EPiServer" />
      </providers>
    </securityEntity>	
  </episerver.framework>
</configuration>
#216280
Edited, Jan 31, 2020 16:11
* 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.