8

I've upgraded my Identity Server project to Net Core 2 and now I am not able to get the iProfileService object to be called to add in custom user claims. It did work in Net Core 1.

Startup.cs ConfigureServices function

// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
services.AddTransient<IProfileService, M25ProfileService>();

//Load certificate
var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, 
"m25id-cert.pfx"), "mypassword");

services.AddIdentityServer()
        .AddSigningCredential(cert)
        .AddConfigurationStore(options =>
        {
            options.ConfigureDbContext = builder =>
                builder.UseSqlServer(connectionString,
                sql => sql.MigrationsAssembly(migrationsAssembly));
        })
        .AddOperationalStore(options =>
        {
            options.ConfigureDbContext = builder =>
                builder.UseSqlServer(connectionString,
                sql => sql.MigrationsAssembly(migrationsAssembly));
                //options.EnableTokenCleanup = true;
                //options.TokenCleanupInterval = 30;
        })
        .AddProfileService<M25ProfileService>();
        .AddAspNetIdentity<ApplicationUser>();

M25ProfileService.cs

public class M25ProfileService : IProfileService
{
    public M25ProfileService(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    public Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var user = _userManager.GetUserAsync(context.Subject).Result;

        var claims = new List<Claim>
        {
            new Claim(JwtClaimTypes.GivenName, user.FirstName),
            new Claim(JwtClaimTypes.FamilyName, user.LastName),
            new Claim(IdentityServerConstants.StandardScopes.Email, user.Email),
            new Claim("uid", user.Id),
            new Claim(JwtClaimTypes.ZoneInfo, user.TimeZone)
        };
        if (user.UserType != null) 
          claims.Add(new Claim("mut", ((int)user.UserType).ToString()));
        context.IssuedClaims.AddRange(claims);
        return Task.FromResult(0);

    }

    public Task IsActiveAsync(IsActiveContext context)
    {
        var user = _userManager.GetUserAsync(context.Subject).Result;
        context.IsActive = user != null;
        return Task.FromResult(0);
    }
}

}

Config.cs

public class Config
{
    // try adding claims to id token
    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        var m25Profile = new IdentityResource(
            "m25.profile", 
            "m25 Profile", 
            new[]
            {
                ClaimTypes.Name,
                ClaimTypes.Email,
                IdentityServerConstants.StandardScopes.OpenId,
                JwtClaimTypes.GivenName,
                JwtClaimTypes.FamilyName,
                IdentityServerConstants.StandardScopes.Email,
                "uid",
                JwtClaimTypes.ZoneInfo
            }
        );

        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResources.Email(),
            m25Profile
        };
    }

    public static IEnumerable<ApiResource> GetApiResources()
    {
        //Try adding claims to access token
        return new List<ApiResource>
        {
            new ApiResource(
                "m25api",
                "message25 API",
                new[]
                {
                    ClaimTypes.Name,
                    ClaimTypes.Email,
                    IdentityServerConstants.StandardScopes.OpenId,
                    JwtClaimTypes.GivenName,
                    JwtClaimTypes.FamilyName,
                    IdentityServerConstants.StandardScopes.Email,
                    "uid",
                    JwtClaimTypes.ZoneInfo
                }
            )
        };
    }

    public static IEnumerable<Client> GetClients()
    {
        // client credentials client
        return new List<Client>
        {
            new Client
            {
                ClientId = "client",
                ClientName = "Client",
                AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,

                ClientSecrets =
                {
                    new Secret("secret".Sha256())
                },
                AllowedScopes = new List<string>
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    IdentityServerConstants.StandardScopes.Email,
                    "m25api"
                }
            },

            // Local Development Client
            new Client
            {
                ClientId = "m25AppDev",
                ClientName = "me25",
                AllowedGrantTypes = GrantTypes.Implicit,
                AllowAccessTokensViaBrowser = true,
                RequireConsent = false,

                RedirectUris = { "http://localhost:4200/authorize.html" },
                PostLogoutRedirectUris = { "http://localhost:4200/index.html" },
                AllowedCorsOrigins = { "http://localhost:4200" },

                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    IdentityServerConstants.StandardScopes.Email,
                    JwtClaimTypes.GivenName,
                    "mut",
                    "m25api"
                },
                AllowOfflineAccess = true,

                IdentityTokenLifetime = 300,
                AccessTokenLifetime = 86400
            }
        };
    }
}

The first thing I'm trying is just to get the identity server to allow me to login and show the user claims similar to the id4 samples. When I login, the standard claims are listed but none of the custom claims. I've put break points in the M25ProfileService class but they never get hit. It seems that ID4 is never using the customer ProfileService class but I do have it in my startup.cs.

I've also tried from my test JS Client and get the same results. Here's a snippet from my JS Client:

var config = {
    authority: "http://localhost:5000",
    client_id: "m25AppDev",
    redirect_uri: "http://localhost:4200/authorize.html",
    response_type: "id_token token",
    scope:"openid profile m25api",
    post_logout_redirect_uri : "http://localhost:4200/index.html"
};
var mgr = new Oidc.UserManager(config);

mgr.getUser().then(function (user) {
    if (user) {
        log("User logged in", user.profile);
        document.getElementById("accessToken").innerHTML = "Bearer " + user.access_token + "\r\n";
    }
    else {
        log("User not logged in");
    }
});

function login() {
    mgr.signinRedirect();
}

At this point, I'm not sure what to try. I thought if I added the claims to the id token (GetIdentityResources() function from what I understand) and even the access token (GetApiResources() function from what I understand), I'd see the claims but nothing seems to work. Please help! Thanks in advance!

Also, I used to be able to get the custom claims from my client as well as from the Identity Server's own index page that renders after log

Okan Karadag
  • 2,542
  • 1
  • 11
  • 23
206mph
  • 439
  • 6
  • 14
  • Are you calling the UserInfo endpoint? Can you show the client configuration? –  Jan 17 '18 at 15:16
  • I just added the client config code from the JS client. However, I can't even get it from the ID server's index page where I'm rendering the claims after login if I go direct to the ID server. – 206mph Jan 17 '18 at 19:51
  • What worked for me was clearing the cookies. – Maryada Purushottam Nov 01 '18 at 21:34

3 Answers3

10

Change the order of these lines of code:

.AddProfileService<M25ProfileService>()
.AddAspNetIdentity<ApplicationUser>();

One if overwriting the other.

Linda Lawton - DaImTo
  • 106,405
  • 32
  • 180
  • 449
leastprivilege
  • 18,196
  • 1
  • 34
  • 50
  • I had the order changed originally but change it thinking that was the problem. I just forgot to change it back. Thanks for pointing that out. Unfortunately, that still doesn't fix the problem. – 206mph Jan 17 '18 at 14:42
  • Do you need to register this as a transient service? Remove that line and try again. Your registering your IProfileService implementation with Identity Server. – Derek Jan 17 '18 at 20:01
  • 1
    Hey Derek, I tried that before and it didn't help. I've seen it in documentation with it registered transient and without registering it. It doesn't seem to make a difference either way. – 206mph Jan 17 '18 at 23:04
8

I figured it out. Thanks to some code on GitHub, I was able to figure out what I was missing. I just needed to add these 2 lines to each client's config in config.cs and all worked perfect!

AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true

This works for remote clients. However, I still can't get it to work when I'm on the ID Server itself logging in (not from a client). That's not a big deal for now but could be something in the future. If/When I figure that piece out, I'll try to remember to update my answer. Meanwhile, I hope this helps others.

206mph
  • 439
  • 6
  • 14
  • When using AlwaysIncludeUserClaimsInIdToken does that mean the claims are included in the header or as a body? An application with 100's of claims could potentially exceed the header size and the request be rejected. Or am i missing something? – Allan Nielsen Mar 11 '19 at 02:28
4

In addition to the answers above (and beside the fact that the Startup.cs shown in the question already contained the relevant line of code) I'd like to add another, yet very simple cause for why the Profile Service might not be called:

Don't forget to register the service with the dependency injection container!

As having just .AddProfileService<ProfileService>() is not enough.

You would also need:

services.AddScoped<IProfileService, ProfileService>();

Or:

services.AddTransient<IProfileService, ProfileService>();
conceptdeluxe
  • 3,753
  • 3
  • 25
  • 29
  • 2
    To me, it was necessary to add "services.AddTransient();" after adding the "AddIdentityServer" – yibe Dec 16 '20 at 02:56
  • 1
    This answer is correct. IProfileService has to be registered through DI, it doesn't work without it on my side. – AndrewSilver Jan 19 '21 at 19:16
  • At least for IdentityServer4 3.1.3 I see that .AddProfileService() is doing just that, so no need to duplicate that entry. – Sérgio Azevedo Jun 28 '23 at 16:03