IdentityServer4 not redirecting to login page on expired authentication

Created on 25 Jan 2018  路  21Comments  路  Source: IdentityServer/IdentityServer4

I've tried StackOverflow and Gitter but I have an issue that has no answer.

Issue / Steps to reproduce the problem

I have a WebForms client that I am trying to setup to use IdentityServer4. The authentication seems to work correctly with one exception: I want the user to be forced to log back in after a certain period of time (for now, just a minute for testing). The tokens appear to timeout correctly and require a call to the IdentityServer code, but IdentityServer always just provides new tokens without requiring the user to enter their credentials again.

Code/Setup

IdentityServer4 2.1.1

StartUp.ConfigureServices()

services.AddIdentity<ApplicationUser, IdentityRole>(options =>
    {
        // TODO: Pull password validation rules from previous implementation
        options.Password.RequireDigit = false;
        options.Password.RequireLowercase = false;
        options.Password.RequireNonAlphanumeric = false;
        options.Password.RequireUppercase = false;
        options.Password.RequiredLength = 4;
        options.Lockout.MaxFailedAccessAttempts = 3;
    })
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    .AddPasswordValidator<AbsPasswordValidation<ApplicationUser>>();

// Add application services.
services.AddTransient<IEmailSender, EmailSender>();

services.AddMvc();

// Configure identity server
services.AddIdentityServer(options =>
   {
       options.Authentication.CookieLifetime = TimeSpan.FromSeconds(60);
       options.Authentication.CookieSlidingExpiration = false;
   })
        .AddSigningCredential(certificate)
        // TODO: Once done configuring swap back to DB from InMemory
        // this adds the config data from DB (clients, resources)
        //.AddConfigurationStore(options => options.ConfigureDbContext = optionsContextBuilder)
        .AddInMemoryClients(Clients.GetClients())
        .AddInMemoryApiResources(Resources.GetApiResources())
        .AddInMemoryIdentityResources(Resources.GetIdentityResources())
        // this adds the operational data from DB (codes, tokens, consents)
        .AddOperationalStore(options => options.ConfigureDbContext = optionsContextBuilder)
        .AddAspNetIdentity<ApplicationUser>()
        .AddProfileService<AbsProfileService>()
        .AddJwtBearerClientAuthentication();

Clients.GetClients()

return new List<Client>
{
    // WebForms Client
    new Client
    {
        ClientName = "ABS",
        ClientId = Abs2Client,
        //ClientSecrets = Secrets,

        AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
        AllowAccessTokensViaBrowser = true,

        AllowedScopes = AllScopes,
        RedirectUris = {"http://localhost:8888/"},
        PostLogoutRedirectUris = {"http://localhost:8888/"},

        // Token lifetimes
        AuthorizationCodeLifetime = 60,
        AccessTokenLifetime = 60,
        IdentityTokenLifetime = 60,

        // Refresh token
        RefreshTokenExpiration = TokenExpiration.Absolute,
        AbsoluteRefreshTokenLifetime = 60,
        SlidingRefreshTokenLifetime = 60,

        RequireConsent = false
    },

WebForms Client (.Net Framework 4.5.2)

StartUp

// Use Cookies to Store JWT Token for Web Browsers
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
    CookieName = "XXX",
    CookieHttpOnly = false,
    ExpireTimeSpan = TimeSpan.FromMinutes(1),
    SlidingExpiration = false
});

JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();

// Authenticate to Auth Server
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
    AuthenticationType = "oidc",
    SignInAsAuthenticationType = "Cookies",
    Authority = "http://localhost:5002/",
    ClientId = "Abs2",
    RedirectUri = "http://localhost:8888/",
    PostLogoutRedirectUri = "http://localhost:8888/",
    ResponseType = "code id_token token",
    Scope = "openid profile AuthApi",
    UseTokenLifetime = false,
    Notifications = new OpenIdConnectAuthenticationNotifications
    {
        SecurityTokenValidated = async n =>
        {
            var claimsToExclude = new[] { "aud", "iss", "nbf", "exp", "nonce", "iat", "at_hash", "c_hash", "idp", "amr" };

            var claimsToKeep = n.AuthenticationTicket.Identity.Claims.Where(x => false == claimsToExclude.Contains(x.Type)).ToList();
            claimsToKeep.Add(new Claim("id_token", n.ProtocolMessage.IdToken));

            if (n.ProtocolMessage.AccessToken != null)
            {
                // Add access_token so we don't need to request it when calling APIs
                claimsToKeep.Add(new Claim("access_token", n.ProtocolMessage.AccessToken));

                var userInfoClient = new UserInfoClient(new Uri(n.Options.Authority + "connect/userinfo").ToString());
                var userInfoResponse = await userInfoClient.GetAsync(n.ProtocolMessage.AccessToken);
                var userInfoClaims = userInfoResponse.Claims
                    .Where(x => x.Type != "sub") // filter sub since we're already getting it from id_token
                    .Select(x => new Claim(x.Type, x.Value));
                claimsToKeep.AddRange(userInfoClaims);
            }

            var ci = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType, "name", "role");
            ci.AddClaims(claimsToKeep);

            n.AuthenticationTicket = new AuthenticationTicket(ci, n.AuthenticationTicket.Properties);
        },
        RedirectToIdentityProvider = n =>
        {
            if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
                n.ProtocolMessage.IdTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token")?.Value;

            return Task.FromResult(0);
        }
    }
});

app.UseStageMarker(PipelineStage.Authenticate);

Relevant parts of the log file

I've included some //comments to where the code was for my own understanding:

2018-01-24 11:04:57.587 -06:00 [Information] Invoking IdentityServer endpoint: "IdentityServer4.Endpoints.AuthorizeEndpoint" for "/connect/authorize"
2018-01-24 11:04:57.592 -06:00 [Debug] Start authorize request
2018-01-24 11:04:57.598 -06:00 [Debug] User in authorize request: "ec4107bd-4bfe-47c9-9f33-42e5a98e5fd6"
2018-01-24 11:04:57.600 -06:00 [Debug] Start authorize request protocol validation
2018-01-24 11:04:57.608 -06:00 [Debug] Checking for PKCE parameters
2018-01-24 11:04:57.609 -06:00 [Debug] No PKCE used.

// var result = await _validator.ValidateAsync(parameters, user);
2018-01-24 11:04:57.634 -06:00 [Debug] Calling into custom validator: "IdentityServer4.Validation.DefaultCustomAuthorizeRequestValidator"

// LogRequest(request);
2018-01-24 11:04:57.769 -06:00 [Information] ValidatedAuthorizeRequest
"{
  \"ClientId\": \"Abs2\",
  \"ClientName\": \"ABS\",
  \"RedirectUri\": \"http://localhost:8888/\",
  \"AllowedRedirectUris\": [
    \"http://localhost:8888/\"
  ],
  \"SubjectId\": \"ec4107bd-4bfe-47c9-9f33-42e5a98e5fd6\",
  \"ResponseType\": \"code id_token token\",
  \"ResponseMode\": \"form_post\",
  \"GrantType\": \"hybrid\",
  \"RequestedScopes\": \"openid profile AuthApi\",
  \"State\": \"OpenIdConnect.AuthenticationProperties=...\",
  \"Nonce\": \"...\",
  \"SessionId\": \"2ccb7fa0ff40ceaff29cbc1d2c3198e3\",
  \"Raw\": {
    \"client_id\": \"Abs2\",
    \"redirect_uri\": \"http://localhost:8888/\",
    \"response_mode\": \"form_post\",
    \"response_type\": \"code id_token token\",
    \"scope\": \"openid profile AuthApi\",
    \"state\": \"OpenIdConnect.AuthenticationProperties=...\",
    \"nonce\": \"...\"
  }
}"

// var interactionResult = await _interactionGenerator.ProcessInteractionAsync(request, consent);
2018-01-24 11:04:57.908 -06:00 [Information] Entity Framework Core "2.0.1-rtm-125" initialized '"ApplicationDbContext"' using provider '"Microsoft.EntityFrameworkCore.SqlServer"' with options: "MigrationsAssembly=Authentication.Host "
2018-01-24 11:04:58.131 -06:00 [Information] Executed DbCommand ("28"ms) [Parameters=["@__get_Item_0='?' (Size = 450)"], CommandType='Text', CommandTimeout='30']"
""SELECT TOP(1) [e].[Id], [e].[AccessFailedCount], [e].[ConcurrencyStamp], [e].[Email], [e].[EmailConfirmed], [e].[LockoutEnabled], [e].[LockoutEnd], [e].[NormalizedEmail], [e].[NormalizedUserName], [e].[PasswordHash], [e].[PhoneNumber], [e].[PhoneNumberConfirmed], [e].[SecurityStamp], [e].[TwoFactorEnabled], [e].[UserName]
FROM [AspNetUsers] AS [e]
WHERE [e].[Id] = @__get_Item_0"
2018-01-24 11:04:58.162 -06:00 [Debug] Client is configured to not require consent, no consent is required

// var response = await _authorizeResponseGenerator.CreateResponseAsync(request);
2018-01-24 11:04:58.165 -06:00 [Debug] Creating Hybrid Flow response.

Most helpful comment

I understand your point, but I am no longer asking a Support question.

As I originally stated, I have already posted on both StackOverflow and Gitter. The consensus being that this should work.

If it should work but does not, then it is a bug, which is why I'm posting here.

What I believe the problem is is that IdentityServer should force a login when presented with expired credentials, but it is not.

I've used both our WebForms client and the MVC client in your sample application, both with the same issue. The cookies in the client timeout and the middleware redirects to IdentityServer. So the issue is with IdentityServer since that code is being called and granting new access without requiring the user to re-login.

I'm sorry I'm not being clear enough for you to understand, by no means am I an IdentityServer expert. But rather than shutting me down, please ask what specifically I need to provide you. Alternately, it should take less time than we've already spent discussing this issue to replicate my results with my repro steps to verify you see what I'm seeing.

All 21 comments

This seems to be a general question about IdentityServer - not a bug report or an issue.

Please use one of the our free or commercial support options

See here for more details.

Thanks!

@brockallen , the first thing I said was that I have tried StackOverflow and Gitter because I've already seen the link you posted. I've actually tried not to post here, but I have run out of options. It's not like I haven't tried to figure this out myself, I've spent a few days on it now.

Since IdentityServer is not operating correctly despite my configuration seeming to be correct, I have posted it here as a bug/issue since that is the only thing I can determine at this point.

but I have run out of options

You have commercial support as an option: http://identityserver.io/#consulting

And if you feel this is a bug, then if you can tel us the steps to repro with our samples that would help confirm it as an issue.

Unfortunately there is no example for WebForms with IdentityServer4 anymore so I used the Combined_AspNetIdentity_and_EntityFrameworkStorage sample project.

Here is what I changed:

IdentityServerWithAspNetIdentity.StartUp (switched to in-memory to make use of config changes and added a 60s expiration to force the auth cookie to expire)

            // configure identity server with in-memory stores, keys, clients and scopes
            services.AddIdentityServer(options =>
                                       {
                                           options.Authentication.CookieLifetime = TimeSpan.FromSeconds(60);
                                           options.Authentication.CookieSlidingExpiration = false;
                                       })
                    .AddDeveloperSigningCredential()
                    .AddAspNetIdentity<ApplicationUser>()
                    .AddInMemoryClients(Config.GetClients())
                    .AddInMemoryApiResources(Config.GetApiResources())
                    .AddInMemoryIdentityResources(Config.GetIdentityResources());
                //// this adds the config data from DB (clients, resources)
                //.AddConfigurationStore(options =>
                //{
                //    options.ConfigureDbContext = builder =>
                //        builder.UseSqlServer(connectionString,
                //            sql => sql.MigrationsAssembly(migrationsAssembly));
                //})
                //// this adds the operational data from DB (codes, tokens, consents)
                //.AddOperationalStore(options =>
                //{
                //    options.ConfigureDbContext = builder =>
                //        builder.UseSqlServer(connectionString,
                //            sql => sql.MigrationsAssembly(migrationsAssembly));

            //    // this enables automatic token cleanup. this is optional.
            //    options.EnableTokenCleanup = true;
            //    options.TokenCleanupInterval = 30;
            //});

IdentityServerWithAspNetIdentity.Config (60s token lifetime)

                new Client
                {
                    ClientId = "mvc",
                    ClientName = "MVC Client",
                    AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,

                    RequireConsent = false,

                    // Token lifetimes
                    AuthorizationCodeLifetime = 60,
                    AccessTokenLifetime = 60,
                    IdentityTokenLifetime = 60,

                    ClientSecrets = 
                    {
                        new Secret("secret".Sha256())
                    },

                    RedirectUris = { "http://localhost:5002/signin-oidc" },
                    PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },

                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        "api1"
                    },
                    AllowOfflineAccess = true
                }

MvcClient.StartUp (60s cookie expiration)

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

            services.AddAuthentication(options =>
                {
                    options.DefaultScheme = "Cookies";
                    options.DefaultChallengeScheme = "oidc";
                })
                .AddCookie("Cookies",
                           options =>
                           {
                               options.ExpireTimeSpan = TimeSpan.FromSeconds(60);
                               options.SlidingExpiration = false;
                           })
                .AddOpenIdConnect("oidc", options =>
                {
                    options.SignInScheme = "Cookies";

                    options.Authority = "http://localhost:5000";
                    options.RequireHttpsMetadata = false;

                    options.ClientId = "mvc";
                    options.ClientSecret = "secret";
                    options.ResponseType = "code id_token";

                    //options.SaveTokens = true;
                    options.GetClaimsFromUserInfoEndpoint = true;

                    options.Scope.Add("api1");
                    options.Scope.Add("offline_access");
                });
        }

Steps
Run the MVC Client and IdentityServer projects
Click 'Secure' link on MVC project
Get redirected to Login page
Enter credentials
Get redirected to MVC
Wait a few minutes
Click 'Secure' link again
Still have access without Login page due to expired credentials

Still have access without Login page due to expired credentials

Did you see what happened in the browser next? Are you getting redirected for OIDC signin? Please do some more digging to explain why this is a bug.

Did you see what happened in the browser next?

Yes, just showed the Secure page again

Are you getting redirected for OIDC signin?

On the initial attempt to access the Secure page yes. On subsequent attempts, outside of the cookie/token expirations, yes but there is no Login page, it just automatically grants access. I can go to 'Home' and back to 'Secure' same results: no login page presented.

Please do some more digging to explain why this is a bug.

I believe I have provided more than enough information as I've been working on this for 3 days. I've done everything you have asked and more. I do not understand why you are refusing to look at this issue, have I done something to personally offend you? If so, please accept my apology I'm only trying to do my job, but I'd ask for you to show some professional courtesy and treat this as you would any other issue report rather than dismissing it as fast as you possibly can.

You're doing your job, yes. Free support for IdentityServer is not my job. That's why for free support we ask people to ask on StackOverflow. We ask that the issue tracker be reserved for bugs and feature requests.

Having said that, it's still not clear to me what you think the problem is. If the MVC cookie times out and then you get signed back in from the SSO provider, then that's working as designed. If the problem is something self-contained with in the MVC app and unrelated to IdentityServer, then that's a question for Microsoft as that's not even our code (we don't do free support for their stuff either).

I'm sorry you feel this is hostile, but by dumping the code above and not being able to point out where in IdentityServer you feel there's a bug, you're asking me to do your work for you. This is why there's commercial support.

I understand your point, but I am no longer asking a Support question.

As I originally stated, I have already posted on both StackOverflow and Gitter. The consensus being that this should work.

If it should work but does not, then it is a bug, which is why I'm posting here.

What I believe the problem is is that IdentityServer should force a login when presented with expired credentials, but it is not.

I've used both our WebForms client and the MVC client in your sample application, both with the same issue. The cookies in the client timeout and the middleware redirects to IdentityServer. So the issue is with IdentityServer since that code is being called and granting new access without requiring the user to re-login.

I'm sorry I'm not being clear enough for you to understand, by no means am I an IdentityServer expert. But rather than shutting me down, please ask what specifically I need to provide you. Alternately, it should take less time than we've already spent discussing this issue to replicate my results with my repro steps to verify you see what I'm seeing.

What I believe the problem is is that IdentityServer should force a login when presented with expired credentials

If the cookie at IdentityServer is still valid, then it won't force a user to re-authenticate. The cookie I think you were configuring above is in the MVC client (not the one within IdentityServer).

Alternately, it should take less time than we've already spent discussing this issue to replicate my results with my repro steps to verify you see what I'm seeing.

So I took the time to try to repro what you did. I changed the IdentityServer cookie to 1 min and no sliding. I change the MVC implicit client to 1 min cookie no sliding. I login, wait 1 min, and try to access secure. I see a login page again.

Thank you, I appreciate you trying it out.

I had another dev try out my repro steps and they were able to confirm the same issue though so to clarify, this is how I set the IdentityServer cookie I believe you are talking about:

services.AddIdentityServer(options =>
   {
       options.Authentication.CookieLifetime = TimeSpan.FromSeconds(60);
       options.Authentication.CookieSlidingExpiration = false;
   })

If that's not how to do it, then mea culpa, clearly I'm off-base and we'll inquire about consulting.

But since I've been able to reproduce it with other people on other machines I have to believe there is something different about your test.

Did you use the ASP.Net Identity example? Because when I use a non-ASP.Net Identity example the login page DOES show up.

The only other thing I can think of is if you used the version of Identity Server 4. Did you use 2.1.1?

@jkeslinke Hi,Budy,How do you solve your question?

I didn't use ASP.Net Identity falling back to our own custom user database instead. Seems like the ASP.Net Identity integration with Identity Server 4 in version 2.1.1 does something that makes the timeouts not work because once I removed it everything worked as expected.

So I would recommend trying different versions and/or dropping ASP.Net Identity if that's an option. Good luck!

@brockallen Have you tried the sliding cookie expire?

@jkeslinke @brockallen
I have the same problem.
But this link solve it.

@jkeslinke , is the problem solved?
I have the same question, and no one can explain why.
I though this is a bug, but did not cause enough attention.

@woailibain I did not find a solution using ASP.Net Identity which seems to be part of the problem. If you can drop that for your own user store that worked for us. Otherwise I'm sorry I don't know the answer.

When using Identity Server 4 with ASP.NET Identity it is the cookie timeout of the Identity Server that comes into play here. After the client token expires the user is re-authenticated against Identity Server and since that token has not expired the client token is renewed. To set the expiration time on the Identity Server you must add the ConfigureApplicationCookiemiddleware in the Identity Server Startup.cs as follows:

services.AddAuthentication();

services.ConfigureApplicationCookie(options =>
    {
        options.Cookie.Expiration = TimeSpan.FromSeconds(60);
        options.ExpireTimeSpan = TimeSpan.FromSeconds(60);
        options.SlidingExpiration = false;
   });

services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_2_1);

@armitagemderivitec it works for absolute expiration time,but it's not work for sliding expiration time,do you have a solution?

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings