0

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.

  • Try do reduce the logging-levels to Debug or Trace and see if that reveals what the problem is? – Tore Nestenius Aug 11 '20 at 10:19
  • What does your Setup.Configure method look like? – Tore Nestenius Aug 11 '20 at 11:49
  • My log level is already on Trace and nothing in there gives me any more information. Because there is nothing logged, I do see a 404 in the IIS log for my request. The Setup.Configure contains the lines I listed above and 1 extra line that logs the end of the System.Configure – Yvonne Arnoldus Aug 11 '20 at 13:51
  • does this help? https://stackoverflow.com/questions/7438250/random-404-using-iis – Tore Nestenius Aug 11 '20 at 14:57

2 Answers2

0

If the error you are experiencing depend on the browser and browser versions, then it could be a SameSite cookie issue. The implementation of SameSite is very buggy in the various browses. Its a big mess!

See this article for a starting point for how to address this.

Tore Nestenius
  • 16,431
  • 5
  • 30
  • 40
0

You have to set the web.config like in this solution OwinStartup not firing

And if is MVC take in count that routes may override the /signin-oidc path I hate MVC5+Net48, MVC+Net7 works really fine, consider migration.

TeChaiMail
  • 21
  • 1
  • 3