0

I have to implement Microsoft.Owin (4.1.0) OIDC authentication on a .NET framework (4.6.1) website, which is accessed in Edge (latest version). Following tutorials I have a basic setup:

  1. Configuration:

         JwtSecurityTokenHandler.DefaultInboundClaimTypeMap = new Dictionary<string, string>();
         AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Email;
         app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
         app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
         app.UseCookieAuthentication(new CookieAuthenticationOptions()
         {
             CookieName = "oidc.default",
             CookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager(),
             LoginPath = new PathString("/Account/Login"),
             //SlidingExpiration = true,
             //ExpireTimeSpan = TimeSpan.FromMinutes(15),
             Provider = new CookieAuthenticationProvider
             {
                 OnResponseSignOut = context =>
                 {
                     CookieKeyStore.Instance.Clear(context.Request.Cookies["oidc.default"]);
                 }
             }
         });
    
  2. Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationOptions:

           oicdAuthOpt.ClientId = "mvc";
           oicdAuthOpt.ClientSecret = "secret";
           oicdAuthOpt.Authority = "http://localhost:5000";
           oicdAuthOpt.RedirectUri = "http://localhost:50990/signin-oidc";
           oicdAuthOpt.PostLogoutRedirectUri = "http://localhost:50990/signout-callback-oidc";
           oicdAuthOpt.ResponseType = "code";
           oicdAuthOpt.Scope = "openid profile";
           oicdAuthOpt.ResponseMode = "query";               
           oicdAuthOpt.AuthenticationType = "OpenIDConnect1";
           oicdAuthOpt.SaveTokens = "false";
           oicdAuthOpt.UseTokenLifetime = "true";
    
  3. TokenValidationParameters:

 oicdAuthOpt.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
                    {
                        SaveSigninToken = true,           // Important to save the token in boostrapcontext
                        ValidateAudience = true,          // Validate the Audience
                        ValidateIssuer = true,            // Validate the Issuer
                        ValidateLifetime = true,          // Validate the tokens lifetime
                        ValidIssuer = authCfg.Authority,             // The issuer to be validated
                        ValidAudience = authCfg.ClientId, // The Audience to be validated
                        RequireExpirationTime = true,
                    };

4.RedirectToIdentityProvider of OpenIdConnectAuthenticationNotifications :

RememberCodeVerifier is based on ScottBrady91.BlogExampleCode

// generate code verifier and code challenge
                        var codeVerifier = CryptoRandom.CreateUniqueId(32);

                        string codeChallenge;
                        using (var sha256 = SHA256.Create())
                        {
                            var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
                            codeChallenge = Base64UrlEncoder.Encode(challengeBytes);
                        }

                        // set code_challenge parameter on authorization request
                        n.ProtocolMessage.Parameters.Add("code_challenge", codeChallenge);
                        n.ProtocolMessage.Parameters.Add("code_challenge_method", "S256");

                        if (AppSettingsKey.MultiFactorAuthEnabled.Enabled)
                            n.ProtocolMessage.AcrValues = authCfg.AcrValues ?? n.ProtocolMessage.AcrValues;

                        // remember code verifier in cookie (adapted from OWIN nonce cookie)
                        RememberCodeVerifier(n, codeVerifier);
                    }
                    logger.Debug("OIDC-Notification: RedirectToIdentityProvider Called");

                    //if signing out, add the id_token_hint
                    if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                    {
                        logger.Debug("  RequestType=" + OpenIdConnectRequestType.Logout);
                        var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");

                        if (idTokenHint != null)
                        {
                            logger.Debug("  IdTokenHint got from n.OwinContext.Authentication.User");
                            n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
                        }
                        logger.Debug("  IdTokenHint=" + n?.ProtocolMessage?.IdTokenHint);
                        
                    }
                    return Task.CompletedTask;

I wanted a few key features, such as saving the cookies with a lifetime and sliding exparation. There are options for it and used according to documentation resulted in always getting authenticated, even after logout. So to make it save tokens, use sliding exparation and also revoke tokens after logout I implemented CookieKeyStore, which stores cookies, checks and updates their experation time and removes them on logout. This is done in a custom System.Web.Mvc.AuthorizeAttribute.

public override void OnAuthorization(AuthorizationContext filterContext)
        {
            try
            {
                if (AppSettingsKey.LoginEnabled.Enabled && AppSettingsKey.OpenIdConnectSSOEnabled.Enabled)
                {
                    var cookie = HttpContext.Current.Request.Cookies["oidc.default"];
                    if (cookie == null)
                    {
                        logger.Debug("oidc.default is null -> HandleUnauthorizedRequest");
                        base.HandleUnauthorizedRequest(filterContext);
                    }
                    else
                    {
                        if (CookieKeyStore.Instance.CheckIfContains(cookie.Value))
                        {
                            if (!CookieKeyStore.Instance.isExpired(cookie.Value))
                            {
                                logger.Debug("oidc.default is not expired:" + cookie.Value + " -> OnAuthorization");
                                //requires oidc.default and ASP.NET_SessionID cookies
                                base.OnAuthorization(filterContext);
                            }
                            else
                            {
                                logger.Debug("oidc.default is expired:" + cookie.Value + " -> HandleUnauthorizedRequest");
                                base.HandleUnauthorizedRequest(filterContext);
                            }
                        }
                        else
                        {
                            logger.Debug("insert oidc.default into the KeyStore:" + cookie.Value + " -> OnAuthorization");
                            CookieKeyStore.Instance.HandleCookies(cookie);
                            base.OnAuthorization(filterContext);
                        }
                    }
                }
                else
                    base.OnAuthorization(filterContext);
            }
            catch (Exception e)
            {
                logger.Error(e, "Exception while overriding the OnAuthorization method.");
            }
        }

I know this is long so far, but there is more. I test with IdentityServer4.Quickstart and with a production server (which I have no access to). The critical point in development was MFA, which to my knowledge only requires acr_values. Before adding acr_values to the configuration, everything works (login, logout, sliding exparation, anti forgery). If I include it in the configuration with my test server I simply get redirected back to the login page after a login request, with production server we get AuthenticationFailed with the following error:

"Value cannot be null. Parameter name: s"

As far as I can tell there is no parameter "s" in context of Authentication, so what is missing?

With acr_values in configuration the only browser that works is Firefox.

At this point it is clear to me that something happens in the background and that conflict is causing my issues. I found other people with conflicts between .NET framework and OWIN, but the workarounds recommended did not work(similar_1, similar_2).

I am truely grateful if you got this far, I welcome any input on this issue.

  • If you're seeing differences in behaviour between browsers it may well be down to cookie handling. Check the console in Edge/Chrome for anything related to cookies being blocked (there are various reasons why). Also, can you expand a bit on your usage of acr_values and maybe provide the full stack trace for the "Value cannot be null" error please? – mackie Jun 30 '21 at 21:55
  • Yes @mackie , cookies are blocked when i am testing in my dev enviroment, but same browser and in production it gives an authentication failed and that error is the exception in AuthenticationFailed notification. I was told all I need to do with "acr_values" is to add it in RedirectToIdentityProvider, thats all I do with it. I cant give you more in detail about the error, because in AuthenticationFailed I only know about the exception, so if there is more info there please tell me. I have no access to the authentication server so I have no way of knowing whats going on that side. – Kristóf Horváth Jul 01 '21 at 06:34
  • Just to clarify, in my dev enviroment with "acr_values" cookies are blocked and nothing happens, in production enviroment we get AuthenticationFailed notification while using the same browser (Edge). Its just an extra fun info that it works fine in Firefox. – Kristóf Horváth Jul 01 '21 at 06:37

0 Answers0