Reputation: 79
.NET framework 4.6.1 website using OIDC authentication (Microsoft.Owin.Security.OpenIdConnect 4.1.0) As part of the authentication I include "code_challenge". The following code is based on this example.
RedirectToIdentityProvider = n =>
{
//ProcessCertificateValidation();
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
if (AppSettingsKey.AuthCodeChallangeEnabled.Enabled)
{
// 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");
// remember code verifier in cookie (adapted from OWIN nonce cookie)
RememberCodeVerifier(n, codeVerifier);
}
if (AppSettingsKey.MultiFactorAuthEnabled.Enabled)
n.ProtocolMessage.AcrValues = authCfg.AcrValues ?? n.ProtocolMessage.AcrValues;
}
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 confirmed that the "codeVerifierCookie" is sent.
AuthorizationCodeReceived = async n =>
{
logger.Debug("OIDC-Notification: AuthorizationCodeReceived Called");
logger.Debug(" Code=" + n.Code);
logger.Debug(" AuthenticationType=" + n.Options.AuthenticationType);
if (authCfg.DiscoverEndpoints)
{
var disco = await n.Options.ConfigurationManager.GetConfigurationAsync(n.OwinContext.Request.CallCancelled);
authCfg.TokenEndpoint = disco.TokenEndpoint;
authCfg.UserinfoEndpoint = disco.UserInfoEndpoint;
authCfg.EndsessionEndpoint = disco.EndSessionEndpoint;
//authCfg.RevocationEndpoint = disco.RevocationEndpoint;
authCfg.WebKeySetEndpoint = disco.JwksUri;
}
if (AppSettingsKey.AuthCodeChallangeEnabled.Enabled) {
var codeVerifier = RetrieveCodeVerifier(n);
// attach code_verifier
n.TokenEndpointRequest.SetParameter("code_verifier", codeVerifier);
}
var requestMessage = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, authCfg.TokenEndpoint);
requestMessage.Content = new System.Net.Http.FormUrlEncodedContent(n.TokenEndpointRequest.Parameters);
var responseMessage = await n.Options.Backchannel.SendAsync(requestMessage);
responseMessage.EnsureSuccessStatusCode();
var responseContent = await responseMessage.Content.ReadAsStringAsync();
Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage message = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage(responseContent);
logger.Debug(" IdToken=" + message.IdToken);
logger.Debug(" AccessToken=" + message.AccessToken);
n.HandleCodeRedemption(message);
},
The issue is that when trying to retrieve the "codeVerifierCookie" it does not exist, when trying to login in Edge or Chrome (on Firefox its there).
Here are the methods used to send, retrieve and get the code verification. CookieManager is configured to be Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager()
.
private void RememberCodeVerifier(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> n, string codeVerifier)
{
var properties = new AuthenticationProperties();
properties.Dictionary.Add("cv", codeVerifier);
n.Options.CookieManager.AppendResponseCookie(
n.OwinContext,
GetCodeVerifierKey(n.ProtocolMessage.State),
Convert.ToBase64String(Encoding.UTF8.GetBytes(n.Options.StateDataFormat.Protect(properties))),
new CookieOptions
{
SameSite = SameSiteMode.None,
HttpOnly = true,
Secure = n.Request.IsSecure,
Expires = DateTime.UtcNow + n.Options.ProtocolValidator.NonceLifetime
});
}
private string RetrieveCodeVerifier(AuthorizationCodeReceivedNotification n)
{
string key = GetCodeVerifierKey(n.ProtocolMessage.State);
string codeVerifierCookie = n.Options.CookieManager.GetRequestCookie(n.OwinContext, key);
if (codeVerifierCookie != null)
{
var cookieOptions = new CookieOptions
{
SameSite = SameSiteMode.None,
HttpOnly = true,
Secure = n.Request.IsSecure
};
n.Options.CookieManager.DeleteCookie(n.OwinContext, key, cookieOptions);
}
var cookieProperties = n.Options.StateDataFormat.Unprotect(Encoding.UTF8.GetString(Convert.FromBase64String(codeVerifierCookie)));
cookieProperties.Dictionary.TryGetValue("cv", out var codeVerifier);
return codeVerifier;
}
private string GetCodeVerifierKey(string state)
{
using (var hash = SHA256.Create())
{
return OpenIdConnectAuthenticationDefaults.CookiePrefix + "cv." + Convert.ToBase64String(hash.ComputeHash(Encoding.UTF8.GetBytes(state)));
}
}
Why does the "codeVerifierCookie" missing when I try to login from Edge or Chrome? Could it be some default setting or maybe my setup is missing something? Why does it work on Firefox?
Thank you for reading my post and I welcome every and any input on the issue.
Upvotes: 0
Views: 934
Reputation: 12961
Have you used HTTPS when testing your app? Cookies that assert SameSite=None
must also be marked as Secure
. I think the issue might be related with Same-Site cookies setting.
I find a thread which has the same issue as yours, you can refer to it. Besides, there's a detailed article about working with SameSite cookies in ASP.NET, you can also check it for more information.
Upvotes: 2