I have an implementation of OpenIdConnect in .NET Framework 4.6.2. Which works fine most of the time but sometimes a 404 is given on /signin-oidc. When you are redirect from IdentityServer4 back to the calling website after loggin in.
If I remove the /signin-oidc from the url in the browser and press enter then I am logged in. This should not happen if the /signin-oidc could not be found because that is what does the login in the the website I believe.
I use IdentityServer4 (with AspNetCore Identity) implementation in .Net Core 3.1 as authority.
The website is running .NET Framework 4.6.2 using the packages:
<package id="Microsoft.IdentityModel.JsonWebTokens" version="6.6.0" targetFramework="net462" />
<package id="Microsoft.IdentityModel.Logging" version="6.6.0" targetFramework="net462" />
<package id="Microsoft.IdentityModel.Protocols" version="6.6.0" targetFramework="net462" />
<package id="Microsoft.IdentityModel.Protocols.OpenIdConnect" version="6.6.0" targetFramework="net462" />
<package id="Microsoft.IdentityModel.Tokens" version="6.6.0" targetFramework="net462" />
<package id="Microsoft.Owin" version="4.1.0" targetFramework="net462" />
<package id="Microsoft.Owin.Host.SystemWeb" version="4.1.0" targetFramework="net462" />
<package id="Microsoft.Owin.Security" version="4.1.0" targetFramework="net462" />
<package id="Microsoft.Owin.Security.Cookies" version="4.1.0" targetFramework="net462" />
<package id="Microsoft.Owin.Security.OpenIdConnect" version="4.1.0" targetFramework="net462" />
I configured openId connect with the following code:
// Clear default.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
// register global exception middle ware to handle owin exceptions
app.Use<OwinExceptionMiddleware>();
app.UseCookieAuthentication(
new CookieAuthenticationOptions
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
LogoutPath = new PathString("/Login.aspx"),
LoginPath = new PathString("/Logout.aspx"),
// We can not use a sliding expiration because some user controls open a request every minute, so with sliding window the cookie would never expire.
ExpireTimeSpan = TimeSpan.FromMinutes(timeOutDuration),
SlidingExpiration = false,
CookieSecure = CookieSecureOption.SameAsRequest,
// Create a cookie authentication provider so we can log when something happens.
Provider = new CookieAuthenticationProvider
{
OnApplyRedirect = context => { _logger.Debug($"Redirect to {context.RedirectUri} from {context.Request.Uri}"); },
OnValidateIdentity = context =>
{
// redirect to re-login for the automatic calls
var seconds = Math.Round(context.Properties.ExpiresUtc?.Subtract(DateTimeOffset.UtcNow).TotalSeconds ?? double.MaxValue, 0, MidpointRounding.AwayFromZero);
if (seconds < 10)
_logger.Debug($"Authentication cookie expires in {seconds} seconds at {DateTime.Now.AddSeconds(seconds)}");
return Task.CompletedTask;
},
OnResponseSignedIn = context => { _logger.Debug($"ResponseSignedIn with {context.Identity?.Name} from {context.Request.Uri}"); },
OnResponseSignIn = context => { _logger.Debug($"ResponseSignIn with {context.Identity?.Name} from {context.Request.Uri}"); },
OnResponseSignOut = context => { _logger.Debug($"ResponseSignOut from {context.Request.Uri}"); },
OnException = context => { _logger.Error(context.Exception, "Could not autenticate with CookieAuthentication"); }
},
// So replace the standard cookie manager because of the fact the application also set cookies
// see https://github.com/aspnet/AspNetKatana/wiki/System.Web-response-cookie-integration-issues for explanation.
CookieManager = new SystemWebChunkingCookieManager {ChunkSize = _cookieChunckSize}
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
// Open Id configuration
AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
// Authority
Authority = _authorityUrl,
RequireHttpsMetadata = false,
// Client
ClientId = _clientId,
ClientSecret = _clientSecret,
// Call back
RedirectUri = _clientRedirectUri,
PostLogoutRedirectUri = _clientPostLogoutRedirectUri,
// OpenID Connect Hybrid Flow, PKCE not supported in .Net Framework only in .Net Code 3.
ResponseType = OpenIdConnectResponseType.CodeIdToken,
// Set scope
Scope = "openid profile role email lastlogindate IdentityServerApi id offline_access",
// keeps id_token smaller
SaveTokens = true,
// make sure the ExpireTimeSpan is used.
UseTokenLifetime = false,
// So replace the standard cookie manager because of the fact the application also set cookies
// see https://github.com/aspnet/AspNetKatana/wiki/System.Web-response-cookie-integration-issues for explanation.
CookieManager = new SystemWebChunkingCookieManager {ChunkSize = _cookieChunckSize},
// set default claim names.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = NameClaimType,
RoleClaimType = RoleClaimType
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
// on receiving authorization code get user information (profile and roles etc) and add to Claims Identity
AuthorizationCodeReceived = async n =>
{
// retrieve a http client.
var client = HttpClientFactory.GetClient(_authorityUrl);
// use the code to get the access and refresh token
var tokenResponse = await client.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest
{
Address = _authorityUrl + AuthenticationService.TokenEndpoint,
GrantType = OpenIdConnectGrantTypes.AuthorizationCode,
ClientId = _clientId,
ClientSecret = _clientSecret,
RedirectUri = _clientRedirectUri,
Code = n.ProtocolMessage.Code
});
_logger.Debug($"Access token expires in {tokenResponse.ExpiresIn} seconds at {DateTime.Now.AddSeconds(tokenResponse.ExpiresIn)}");
if (tokenResponse.IsError)
{
_logger.Error($"Could not retrieve access_token: {tokenResponse.Error}");
return;
}
// use the access token to retrieve claims from user info
var userInfoResponse = await client.GetUserInfoAsync(new UserInfoRequest
{
Address = _authorityUrl + AuthenticationService.UserInfoEndpoint,
Token = tokenResponse.AccessToken
});
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType,NameClaimType, RoleClaimType);
// add user properties to claims
if (userInfoResponse.IsError)
{
_logger.Error($"Could not retrieve user information: {userInfoResponse.Error}");
return;
}
// set user information (in Microsoft format) in claims
if (userInfoResponse.Claims.Any())
id.AddClaims(userInfoResponse.GetMicrosoftClaims());
// add tokens to the claims collection
if (!string.IsNullOrEmpty(tokenResponse.AccessToken))
id.AddClaim(new Claim(OpenIdConnectParameterNames.AccessToken, tokenResponse.AccessToken));
if (!string.IsNullOrEmpty(n.ProtocolMessage.IdToken))
id.AddClaim(new Claim(OpenIdConnectParameterNames.IdToken, n.ProtocolMessage.IdToken));
if (!string.IsNullOrEmpty(tokenResponse.RefreshToken))
id.AddClaim(new Claim(OpenIdConnectParameterNames.RefreshToken, tokenResponse.RefreshToken));
if (tokenResponse.ExpiresIn != default)
{
var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn);
id.AddClaim(new Claim("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture)));
}
// add profile to claims
var userId = id.GetClaim<string>("id");
if (string.IsNullOrEmpty(userId))
{
_logger.Error("No user id found in the claims, which should not happen.");
return;
}
// retrieve profile
var apiClient = IdentityServerClientFactory.GetApiClient();
var profile = await apiClient.GetProfileAsync(new ProfileRequest
{Token = tokenResponse.AccessToken, UserId = userId});
if (profile == null)
{
_logger.Error($"Profile is null for user {userId}, which should not happen.");
return;
}
id.AddClaims(profile.ToClaims());
// create a new authentication ticket.
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, NameClaimType, RoleClaimType),
n.AuthenticationTicket.Properties);
},
// Set IdTokenHint on logout, so we can logout on Identity Server.
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType != OpenIdConnectRequestType.Logout)
return Task.CompletedTask;
var idToken = n.OwinContext.Authentication.User.FindFirst("id_token")?.Value;
n.ProtocolMessage.IdTokenHint = idToken;
return Task.CompletedTask;
},
// Log on authentication failed.
AuthenticationFailed = n =>
{
// hack for: Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolInvalidNonceException: IDX21323: RequireNonce is '[PII is hidden]'
if (n.Exception.Message.Contains("IDX21323"))
{
n.SkipToNextMiddleware();
return Task.FromResult(0);
}
_logger.Error(n.Exception, "Could not autenticate with OpenIdConnect");
return Task.CompletedTask;
}
}
});
The headers when a 404 occurs are the following:
**General:**
Request URL: https://ipp***.***.nl:*3/signin-oidc
Request Method: POST
Status Code: 404
Remote Address: 89.**.**.**:*3
Referrer Policy: no-referrer
**Response Headers:**
access-control-allow-origin: *
content-length: 1245
content-type: text/html
date: Tue, 11 Aug 2020 08:45:20 GMT
server: Microsoft-IIS/10.0
status: 404
**Request Headers:**
:authority: ipp***.***.nl:*3
:method: POST
:path: /signin-oidc
:scheme: https
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9,nl;q=0.8
cache-control: max-age=0
content-length: 1483
content-type: application/x-www-form-urlencoded
cookie: _ga=GA1.2.1680329711.1591349847; ASP.NET_SessionId=j4kjjmlbas2ujfdcgewwe1yz
origin: null
sec-fetch-dest: document
sec-fetch-mode: navigate
sec-fetch-site: same-site
sec-fetch-user: ?1
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
**Form Data:**
code: lNFyWNLLi1QomK....
id_token: eyJhbGciOiJSUzI1NiIsImtpZCI6...
scope: openid profile role email lastlogindate id IdentityServerApi offline_access
state: OpenIdConnect.AuthenticationProperties=VxS5f80S8nNTcArI8n91rOOf58R8BUn3xHPVdz....
I for the life of me can't figure out why sometimes an 404 is thrown. There is no error in the log of Identity Server and there is no error in the website log because the call never gets there because of the 404.
Update:
On an older browser IE11 (version 11.0.10240.18638) on Windows 10 Enterprise 2015 LTSB, this browser is the newest browser for that OS. I have the 404 all the time on /signin-oidc. What I found there is that the OpenIdConnect.nonce cookie is not present when calling /signin-oidc so that either in that browser the cookie comes not back for IdentityServer4 or gets lost. Could not attach fiddler to that version of IE11 to figure out what is the case.