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:
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"]); } } });
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";
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.