2

For the last couple of weeks, I have been working on a new identity-provider for one of our systems. This is the first time, that I work with IdentityServer and OpenId, and I have managed to get the most of it working, but I keep hitting a concrete wall with this one.

What I have been trying to make

Our goal, is to have one identity provider (IdentityServer4) that we can use for all of our current and future systems. We want this to be as simple as possible, so it does only validate the user, and serves name, profilepicture and e-mail to the clients.

Next, we have another IdentityServer4 server per system, that handles the users access rights. Meaning, this will be supplying the clients with roles and such.

Finally, we have the clients.

I have tried to illustrate, what we wish to have in the long run, but this question is only regarding ID. PROVIDER, SYSTEM A IDSRV, and SYSTEM A CLIENT 1.

|---- SYSTEM A CLIENT 1 |----SYSTEM A IDSRV ----|---- SYSTEM A CLIENT 2 | |---- SYSTEM A CLIENT 3 | | | |---- SYSTEM B CLIENT 1 ID. PROVIDER----|----SYSTEM B IDSRV ----|---- SYSTEM B CLIENT 2 | |---- SYSTEM B CLIENT 3 | | | |---- SYSTEM C CLIENT 1 |----SYSTEM C IDSRV ----|---- SYSTEM C CLIENT 2 |---- SYSTEM C CLIENT 3

The initial autorization works perfect!

What I have

ID. PROVIDER -> Client configuration for SYSTEM A IDSRV

new Client
{
    ClientId = "SystemA",
    ClientSecrets = new List<Secret>
    {
        new Secret("SystemASecret".Sha256())
    },
    AllowedGrantTypes = GrantTypes.Hybrid,
    AllowedScopes = new List<string>
    {
        StandardScopes.OpenId.Name,
        StandardScopes.Profile.Name,
        StandardScopes.Email.Name,
        StandardScopes.OfflineAccess.Name,
    },
    RedirectUris = new List<string>
    {
        "https://localhost:5400/signin-oidc"
    },
    PostLogoutRedirectUris = new List<string>
    {
        "http://localhost:5400"
    },
    RequireConsent = false,

    RefreshTokenUsage = TokenUsage.OneTimeOnly,
    RefreshTokenExpiration = TokenExpiration.Absolute,
    UpdateAccessTokenClaimsOnRefresh = true,

    IdentityTokenLifetime = 30,
    AccessTokenLifetime = 30,
    AuthorizationCodeLifetime = 30,
    AbsoluteRefreshTokenLifetime = 30,
    SlidingRefreshTokenLifetime = 30,
}

SYSTEM A IDSRV -> Startup.cs:

app.UseIdentityServer();

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
    AutomaticAuthenticate = false,
    AutomaticChallenge = false,

    LoginPath = new PathString("/Account/Login"),
    ReturnUrlParameter = "returnUrl"
});

app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
    SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
    SignOutScheme = IdentityServerConstants.SignoutScheme,

    DisplayName = "ID. PROVIDER",
    Authority = "https://localhost:5200",
    ClientId = "SystemA",
    ClientSecret = "SystemASecret",

    ResponseType = "code id_token",

    Scope =
    {
        "openid",
        "profile",
        "email",
        "offline_access"
    },

    TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
    {
        NameClaimType = "name",
        RoleClaimType = "role"
    },

    GetClaimsFromUserInfoEndpoint = true,
    SaveTokens = true,
    UseTokenLifetime = true
});

SYSTEM A IDSRV -> Client configuration for SYSTEM A CLIENT 1:

new Client
{
    ClientId = "SystemAClient1",
    ClientName = "SYSTEM A CLIENT 1",
    AllowedGrantTypes = GrantTypes.Hybrid,

    RequireConsent = false,

    ClientSecrets = new List<Secret>
    {
        new Secret("SystemAClient1Secret".Sha256())
    },
    RedirectUris = new List<string>
    {
        "https://localhost:5500/signin-oidc"
    },
    PostLogoutRedirectUris = new List<string>
    {
        "https://localhost:5500"
    },
    AllowedScopes = new List<string>
    {
        StandardScopes.OpenId.Name,
        StandardScopes.Profile.Name,
        StandardScopes.Email.Name,
        StandardScopes.OfflineAccess.Name,
        StandardScopes.Roles.Name,
        CustomScopes.Custom1.Name,
        CustomScopes.Custom2.Name,
        CustomScopes.Custom3.Name
    },

    RefreshTokenUsage = TokenUsage.OneTimeOnly,
    RefreshTokenExpiration = TokenExpiration.Absolute,
    UpdateAccessTokenClaimsOnRefresh = true,

    IdentityTokenLifetime = 30,
    AccessTokenLifetime = 30,
    AuthorizationCodeLifetime = 30,
    AbsoluteRefreshTokenLifetime = 30,
    SlidingRefreshTokenLifetime = 30
}

*SYSTEM A IDSRV -> AccountController (ExternalCallback when signing in, inspired by IdentityServer samples.)

    public async Task<IActionResult> ExternalCallback(string returnUrl)
    {
    //Read external identity from the tempoary cookie
    var tempUser = await HttpContext.Authentication.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
    if (tempUser == null)
        throw new Exception("External authentication error");

    //Get external claims
    var claims = tempUser.Claims.ToList();

    //Extract UserId
    var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject);
    if (userIdClaim == null)
        throw new Exception("Unknown UserId!");

    //Move the UserId claim and from the claims collection to the UserId-property, and set the name of the external authentication provider
    claims.Remove(userIdClaim);
    var provider = userIdClaim.Issuer;

    //Extract addition custom claims to store them in the database
    var firstnameClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.GivenName);
    if (firstnameClaim == null)
        throw new Exception("Firstname claim missing");

    var lastnameClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.FamilyName);
    if (lastnameClaim == null)
        throw new Exception("Lastname claim missing!");

    var emailClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Email);
    if (emailClaim == null)
        throw new Exception("E-mail claim missing!");

    var pictureClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Picture);
    if (pictureClaim == null)
        throw new Exception("Picture claim missing!");

    var user = await _userService.FindUserByEmail(emailClaim.Value);

    if (user == null)
        user = new User { Identities = new List<UserIdentity>() };

    user.Email = emailClaim.Value;
    user.Firstname = firstnameClaim.Value;
    user.Lastname = lastnameClaim.Value;
    user.Picture = pictureClaim.Value;

    if (!user.Identities.Any(i => i.Identity == userIdClaim.Value && i.Provider == provider))
        user.Identities.Add(new UserIdentity { Identity = userIdClaim.Value, Provider = provider });

    // Save user and login
    await _userService.SaveUser(user);
    await HttpContext.Authentication.SignInAsync(userIdClaim.Value, emailClaim.Value, provider, claims.ToArray());

    // Delete tempoary cookie used during external authentication
    await HttpContext.Authentication.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);

    if (_interaction.IsValidReturnUrl(returnUrl))
        return Redirect(returnUrl);

    return Redirect("~/");
}

SYSTEM A CLIENT 1 -> Startup.cs

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationScheme = "Cookies",
});

app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
    SignInScheme = "Cookies",

    Authority = "https://localhost:5400",

    ClientId = "SystemAClient1",
    ClientSecret = "SystemAClient1Secret",

    ResponseType = "code id_token",
    Scope =
    {
        "openid",
        "profile",
        "email",
        "offline_access"
    },

    GetClaimsFromUserInfoEndpoint = true,
    SaveTokens = true,
    UseTokenLifetime = true,
});

The problem

When I refreshes a page on SYSTEM A CLIENT 1 after the 30 secs refresh interval, i does refresh the access token and claims on SYSTEM A IDSRV as it should. But that call does not refresh the access token and claims on ID. PROVIDER.

What am I missing?

tl;dr

IdentityServer4 does not refresh access token from external provider when client initiates access token refresh.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Nicky
  • 428
  • 3
  • 13

0 Answers0