1

I have a standard .NET Core 2.1 (MVC and API) and Identity Server 4 project setup. I am using reference tokens instead of jwt tokens.

The scenario is as follows:

  • Browse to my application
  • Redirected to Identity Server
  • Enter valid valid credentials
  • Redirected back to application with all claims (roles) and correct access to the application and API

Wait an undetermined amount of time (I think it's an hour, I don't have the exact timing)

  • Browse to my application
  • Redirected to Identity Server
  • I'm still logged into the IDP so I'm redirected immediately back to my application
  • At this point the logged in .NET user is missing claims (roles) and no longer has access to the API

The same result happens if I delete all application cookies

It seems obvious to me that the access token has expired. How do I handle this scenario? I'm still logged into the IDP and the middleware automatically logged me into my application, however, with an expired (?) access token and missing claims.

Does this have anything to do with the use of reference tokens?

I'm digging through a huge mess of threads and articles, any guidance and/or solution to this scenario?

EDIT: It appears my access token is valid. I have narrowed my issue down to the missing user profile data. Specifically, the role claim.

When I clear both my application and IDP cookies, everything works fine. However, after "x" (1 hour?) time period, when I attempt to refresh or access the application I am redirected to the IDP then right back to the application.

At that point I have a valid and authenticated user, however, I am missing all my role claims.

How can I configure the AddOpenIdConnect Middleware to fetch the missing claims in this scenario?

I suppose in the OnUserInformationReceived event I can check for the missing "role" claim, if missing then call the UserInfoEndpoint...that seems like a very odd workflow. Especially since on a "fresh" login the "role" claim comes back fine. (Note: I do see the role claim missing from the context in the error scenario).

Here is my client application configuration:

services.AddAuthentication(authOpts =>
        {
            authOpts.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            authOpts.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, opts => { })
        .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, openIdOpts =>
        {
            openIdOpts.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            openIdOpts.Authority = settings.IDP.Authority;
            openIdOpts.ClientId = settings.IDP.ClientId;
            openIdOpts.ClientSecret = settings.IDP.ClientSecret;
            openIdOpts.ResponseType = settings.IDP.ResponseType;

            openIdOpts.GetClaimsFromUserInfoEndpoint = true;
            openIdOpts.RequireHttpsMetadata = false;
            openIdOpts.SaveTokens = true;
            openIdOpts.ResponseMode = "form_post";

            openIdOpts.Scope.Clear();
            settings.IDP.Scope.ForEach(s => openIdOpts.Scope.Add(s));

            // https://leastprivilege.com/2017/11/15/missing-claims-in-the-asp-net-core-2-openid-connect-handler/
            // https://github.com/aspnet/Security/issues/1449
            // https://github.com/IdentityServer/IdentityServer4/issues/1786

            // Add Claim Mappings
            openIdOpts.ClaimActions.MapUniqueJsonKey("preferred_username", "preferred_username"); /* SID alias */
            openIdOpts.ClaimActions.MapJsonKey("role", "role", "role");

            openIdOpts.TokenValidationParameters = new TokenValidationParameters
            {
                ValidAudience = settings.IDP.ClientId,
                ValidIssuer = settings.IDP.Authority,
                NameClaimType = "name",
                RoleClaimType = "role"
            };

            openIdOpts.Events = new OpenIdConnectEvents
            {
                OnUserInformationReceived = context =>
                {
                    Log.Info("Recieved user info from IDP.");
                    // check for missing roles?  they are here on a fresh login but missing
                    // after x amount of time (1 hour?)
                    return Task.CompletedTask;
                },
                OnRedirectToIdentityProvider = context =>
                {
                    Log.Info("Redirecting to identity provider.");
                    return Task.CompletedTask;
                },
                OnTokenValidated = context =>
                    {
                        Log.Debug("OnTokenValidated");
                        // this addressed the scenario where the Identity Server validates a user however that user does not
                        // exist in the currently configured source system.
                        // Can happen if there is a configuration mismatch between the local SID system and the IDP Client
                        var validUser = false;
                        int uid = 0;
                        var identity = context.Principal?.Identity as ClaimsIdentity;

                        if (identity != null)
                        {
                            var sub = identity.Claims.FirstOrDefault(c => c.Type == "sub");

                            Log.Debug($"    Validating sub '{sub.Value}'");

                            if (sub != null && !string.IsNullOrWhiteSpace(sub.Value))
                            {

                                if (Int32.TryParse(sub.Value, out uid))
                                {
                                    using (var configSvc = ApiServiceHelper.GetAdminService(settings))
                                    {
                                        try
                                        {
                                            var usr = configSvc.EaiUser.GetByID(uid);

                                            if (usr != null && usr.ID.GetValueOrDefault(0) > 0)
                                                validUser = true;
                                        }
                                        catch { }
                                    }
                                }
                            }

                            Log.Debug($"    Validated sub '{sub.Value}'");
                        }

                        if (!validUser)
                        {
                            // uhhh, does this work?  Logout?
                            // TODO: test!
                            Log.Warn($"Unable to validate user is SID for ({uid}).  Redirecting to '/Home/Logout'");
                            context.Response.Redirect("/Home/Logout?msg=User not validated in source system");
                            context.HandleResponse();
                        }

                        return Task.CompletedTask;
                    },
                OnTicketReceived = context =>
                {
                    // TODO: Is this necessary?
                    // added the below code because I thought my application access_token was expired
                    // however it turns out I'm actually misisng the role claims when I come back to the
                    // application from the IDP after about an hour
                    if (context.Properties != null &&
                        context.Properties.Items != null)
                    {
                        DateTime expiresAt = System.DateTime.MinValue;

                        foreach (var p in context.Properties.Items)
                        {
                            if (p.Key == ".Token.expires_at")
                            {
                                DateTime.TryParse(p.Value, null, DateTimeStyles.AdjustToUniversal, out expiresAt);
                                break;
                            }
                        }

                        if (expiresAt != DateTime.MinValue &&
                            expiresAt != DateTime.MaxValue)
                        {
                            // I did this to synch the .NET cookie timeout with the IDP access token timeout?
                            // This somewhat concerns me becuase I thought that part should be done auto-magically already
                            // I mean, refresh token?
                            context.Properties.IsPersistent = true;
                            context.Properties.ExpiresUtc = expiresAt;
                        }
                    }

                    return Task.CompletedTask;
                }
            };
        });
user1949561
  • 187
  • 1
  • 13
  • Possible duplicate of [IdentityServer4 - Using Refresh Tokens after following the Quickstart for Hybrid MVC](https://stackoverflow.com/questions/41741982/identityserver4-using-refresh-tokens-after-following-the-quickstart-for-hybrid) –  Sep 21 '18 at 05:21
  • Thanks so much @RuardvanElburg! I appreciate your time and contributions!! I reviewed the link you sent and sadly that is not my issue. Renewing the tokens before I call out to the API is working fine for me. My apologies for being unclear in the original posting. I have added an edit to attempt to clarify my issue. – user1949561 Sep 21 '18 at 12:16

1 Answers1

0

I'm sorry folks, looks like I found the source of my issue.

Total fail on my side :(.

I had a bug in the ProfileService in my Identity Server implementation that was causing the roles to not be returned in all cases

humph, thanks!

user1949561
  • 187
  • 1
  • 13