chronolinq
chronolinq

Reputation: 103

OpenIdConnectAuthentication cookie is never issued

We have a .net MVC web application that uses ASP.NET Membership for user authentication. We also issue bearer tokens for our mobile application. I am attempting to add support for SSO using okta as an example. I have created a application in Okta and am attempting to sign into our application. My current flow is as follows:

  1. Open login page
  2. Click on "login with okta"
  3. Request received by our server. User is not authenticated, so a challenge is issued
  4. User is redirected to Okta's login page
  5. After successful login, a request is made to the login redirect url (which I suppose is captured by the OpenIdConnect middleware?)
  6. The AuthorizationCodeReceived event is triggered.
  7. The redirect completes, but no cookie is issued
  8. The user is redirected to the login page, but because they are not authenticated, it redirects to Okta to sign in. Loop back to step 4 and repeat.

It seems that after the login redirect and I set the correct claims, the result should be that a cookie should be issued and returned on the response to denote that the user is signed in. I'm not sure what I'm missing here, but if anyone can point me in the right direction, or explain what is happening I would very much appreciate it!

Below is a snippet from our Startup.cs:

public void ConfigureAuth(IAppBuilder app, bool allowInsecureHttp = false)
{
    // Configure the db context, user manager and signin manager to use a single instance per request
    app.CreatePerOwinContext(OwinIdentityDbContext.Create);
    app.CreatePerOwinContext<IdentityUserManager>(IdentityUserManager.Create);
    app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

    app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
    {
        ClientId = GlobalConstants.Settings.OktaClientId,
        ClientSecret = GlobalConstants.Settings.OktaClientSecret,
        Authority = GlobalConstants.Settings.OktaDomain,
        AuthenticationType = "okta", // This is basically a "name" for this open id configuration
        RedirectUri = GlobalConstants.Settings.OktaRedirectUri,
        ResponseType = OpenIdConnectResponseType.CodeIdToken,
        Scope = $"{OpenIdConnectScope.OpenIdProfile} {OpenIdConnectScope.Email}", // Get the profile and email
        PostLogoutRedirectUri = GlobalConstants.Settings.OktaPostLogoutRedirectUri,
        TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = "name"
        },

        Notifications = new OpenIdConnectAuthenticationNotifications
        {
            AuthorizationCodeReceived = async n =>
            {
                // Exchange code for access and ID tokens
                var tokenClient = new TokenClient(GlobalConstants.Settings.OktaDomain + "/v1/token", GlobalConstants.Settings.OktaClientId, GlobalConstants.Settings.OktaClientSecret);
                var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, GlobalConstants.Settings.OktaRedirectUri);

                if (tokenResponse.IsError)
                {
                    throw new Exception(tokenResponse.Error);
                }

                var userInfoClient = new UserInfoClient(GlobalConstants.Settings.OktaDomain + "/v1/userinfo");
                var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
                var claims = new List<Claim>();
                claims.AddRange(userInfoResponse.Claims);
                claims.Add(new Claim("id_token", tokenResponse.IdentityToken));
                claims.Add(new Claim("access_token", tokenResponse.AccessToken));

                if (!string.IsNullOrEmpty(tokenResponse.RefreshToken))
                {
                    claims.Add(new Claim("refresh_token", tokenResponse.RefreshToken));
                }

                // Add custom user claims here
                #region Add custom claims

                var email = claims.Find(_ => _.Type == "email")?.Value;

                claims.Add(new Claim(ClaimTypes.Name, email));
                claims.Add(new Claim(ClaimTypes.NameIdentifier, email));

                // Hardcoded for now. Discover these for real later
                claims.Add(new Claim(CustomClaimTypes.OurUserId, "{userId}"));
                claims.Add(new Claim(CustomClaimTypes.OurOrganizationId, "{orgId}"));

                #endregion

                // https://stackoverflow.com/questions/55797063/asp-net-owin-openid-connect-not-creating-user-authentication
                var identity = new ClaimsIdentity(DefaultAuthenticationTypes.ApplicationCookie, ClaimTypes.Name, ClaimTypes.Role);
                n.AuthenticationTicket = new AuthenticationTicket(identity, n.AuthenticationTicket.Properties);
                n.AuthenticationTicket.Identity.AddClaims(claims);

                return;
            },

            RedirectToIdentityProvider = n =>
            {
                // If signing out, add the id_token_hint
                if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                {
                    var idTokenClaim = n.OwinContext.Authentication.User.FindFirst("id_token");

                    if (idTokenClaim != null)
                    {
                        n.ProtocolMessage.IdTokenHint = idTokenClaim.Value;
                    }
                }

                return Task.CompletedTask;
            }
        },
    });

    app.UseCookieAuthentication(
        new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            CookieManager = new SystemWebCookieManager(),
            LoginPath = new PathString("/"),
            SlidingExpiration = true,
            CookieSecure = GlobalConstants.Settings.IsEnvironmentDev ? CookieSecureOption.SameAsRequest : CookieSecureOption.Always,
            Provider = new CookieAuthenticationProvider
            {
                OnApplyRedirect = context =>
                {
                    if (IsClientPortalRequest(context.Request))
                    {
                        context.Response.Redirect("/public/ClientPortalLogin");
                    }
                    else if (!IsAjaxRequest(context.Request) && !IsPublicApiRequest(context.Request) && !IsWopiRequest(context.Request))
                    {
                        context.Response.Redirect(context.RedirectUri);
                    }
                }
            }
        });

    // For Two Factor Log In
    app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(10));

    OAuthOptions = new OAuthAuthorizationServerOptions
    {
        AllowInsecureHttp = allowInsecureHttp, // NOTE: Only for unit tests
        TokenEndpointPath = new PathString("/token"),
        AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
        Provider = new CustomBearerTokenProvider()
    };

    // Configure Bearer Token Generation
    app.UseOAuthAuthorizationServer(OAuthOptions);

    // Needed to Authenticate Bearer Tokens
    app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

    // Set Cookies are the default type
    app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationTypes.ApplicationCookie);
}

Here is a screenshot of our okta application configuration: enter image description here

Upvotes: 0

Views: 1904

Answers (1)

StarGazer
StarGazer

Reputation: 21

I had the same problem and this is what I discovered. The issue was not in my wire up code, it was actually in my controller class. I had an attribute on my controller action that was requiring the user to be in certain roles. This is ultimately what caused my redirect loop.

If you have that, you need to get rid of it and find a different way of handling the roles.

This shows the offending code: Circled code is what I removed.

Here is the link to the article that caused me to have my "aha" moment:

https://www.blinkingcaret.com/2016/01/20/authorization-redirect-loops-asp-net-mvc/

Hope this helps someone, took me too long to figure this out.

Upvotes: 2

Related Questions