7

I've read the docs and followed the examples but I am unable to get user claims into the access token. My client is not ASP.NET core, so the configuration of the MVC client is not the same as the v4 samples.

Unless I have misunderstood the docs, the ApiResources are used to populate the RequestedClaimTypes in the profile service when creating the access token. The client should add the api resource to it's list of scopes to include associated userclaims. In my case they are not being connected.

When ProfileService.GetProfileDataAsync is called with a caller of "ClaimsProviderAccessToken", the requested claim types are empty. Even if I set the context.IssuedClaims in here, when it is called again for "AccessTokenValidation" the claims on the context are not set.

In the MVC app:

    app.UseOpenIdConnectAuthentication(
            new OpenIdConnectAuthenticationOptions
            {
                UseTokenLifetime = false, 
                ClientId = "portal",
                ClientSecret = "secret",
                Authority = authority,
                RequireHttpsMetadata = false,
                RedirectUri = redirectUri,
                PostLogoutRedirectUri = postLogoutRedirectUri,
                ResponseType = "code id_token",
                Scope = "openid offline_access portal",
                SignInAsAuthenticationType = "Cookies",
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthorizationCodeReceived = async n =>
                    {
                        await AssembleUserClaims(n);
                    },
                    RedirectToIdentityProvider = n =>
                    {
                        // if signing out, add the id_token_hint
                        if (n.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectRequestType.Logout)
                        {
                            var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");

                            if (idTokenHint != null)
                            {
                                n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
                            }

                        }

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

    private static async Task AssembleUserClaims(AuthorizationCodeReceivedNotification notification)
    {

        string authCode = notification.ProtocolMessage.Code;

        string redirectUri = "https://myuri.com";

        var tokenClient = new TokenClient(tokenendpoint, "portal", "secret");

        var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(authCode, redirectUri);

        if (tokenResponse.IsError)
        {
            throw new Exception(tokenResponse.Error);
        }

        // use the access token to retrieve claims from userinfo
        var userInfoClient = new UserInfoClient(new Uri(userinfoendpoint), tokenResponse.AccessToken);

        var userInfoResponse = await userInfoClient.GetAsync();

        // create new identity
        var id = new ClaimsIdentity(notification.AuthenticationTicket.Identity.AuthenticationType);
        id.AddClaims(userInfoResponse.GetClaimsIdentity().Claims);
        id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
        id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
        id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
        id.AddClaim(new Claim("id_token", notification.ProtocolMessage.IdToken));
        id.AddClaim(new Claim("sid", notification.AuthenticationTicket.Identity.FindFirst("sid").Value));
        notification.AuthenticationTicket = new AuthenticationTicket(id, notification.AuthenticationTicket.Properties);
    }

Identity Server Client:

    private Client CreatePortalClient(Guid tenantId)
    {
        Client portal = new Client();
        portal.ClientName = "Portal MVC";
        portal.ClientId = "portal";
        portal.ClientSecrets = new List<Secret> { new Secret("secret".Sha256()) };
        portal.AllowedGrantTypes = GrantTypes.HybridAndClientCredentials;
        portal.RequireConsent = false; 
        portal.RedirectUris = new List<string> {
            "https://myuri.com",
        };
        portal.AllowedScopes = new List<string>
        {
                IdentityServerConstants.StandardScopes.OpenId,
                IdentityServerConstants.StandardScopes.Profile,
                "portal"
        };
        portal.Enabled = true;
        portal.AllowOfflineAccess = true;
        portal.AlwaysSendClientClaims = true;
        portal.AllowAccessTokensViaBrowser = true;

        return portal;
    }

The API resource:

public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource
            {
                Name= "portalresource",
                UserClaims = { "tenantId","userId","user" }, 
                Scopes =
                {
                    new Scope()
                    {
                        Name = "portalscope",
                        UserClaims = { "tenantId","userId","user",ClaimTypes.Role, ClaimTypes.Name),

                    },

                }
            },

        };
    }

The Identity resource:

    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new IdentityResource[]
        {
            // some standard scopes from the OIDC spec
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResources.Email(),
            new IdentityResource("portal", new List<string>{ "tenantId", "userId", "user", "role", "name"})
        };
    }

UPDATE:

Here is the interaction between the MVC app and the Identity Server (IS):

MVC: 
    Owin Authentication Challenge
IS:
    AccountController.LoginAsync - assemble user claims and call HttpContext.SignInAsync with username and claims)
    ProfileService.IsActiveAsync - Context = "AuthorizeEndpoint", context.Subject.Claims = all userclaims
    ClaimsService.GetIdentityTokenClaimsAsync - Subject.Claims (all userclaims), resources = 1 IdentityResource (OpenId), GrantType = Hybrid
MVC:
    SecurityTokenValidated (Notification Callback)
    AuthorizationCodeReceived - Protocol.Message has Code and IdToken call to TokenClient.RequestAuthorizationCodeAsync()
IS: 
    ProfileService.IsActiveAsync - Context = "AuthorizationCodeValidation", context.Subject.Claims = all userclaims
    ClaimsService.GetAccessTokenClaimsAsync - Subject.Claims (all userclaims), resources = 2 IdentityResource (openId,profile), GrantType = Hybrid
    ProfileService.GetProfileDataAsync - Context = "ClaimsProviderAccessToken", context.Subject.Claims = all userclaims, context.RequestedClaimTypes = empty, context.IssuedClaims = name,role,user,userid,tenantid
    ClaimsService.GetIdentityTokenClaimsAsync - Subject.Claims (all userclaims), resources = 2 IdentityResource (openId,profile), GrantType = authorization_code

MVC:
    call to UserInfoClient with tokenResponse.AccessToken
IS:
    ProfileService.IsActiveAsync - Context = "AccessTokenValidation", context.Subject.Claims = sub,client_id,aud,scope etc (expecting user and tenantId here)
    ProfileService.IsActiveAsync - Context = "UserInfoRequestValidation", context.Subject.Claims = sub,auth_time,idp, amr
    ProfileService.GetProfileDataAsync - Context = "UserInfoEndpoint", context.Subject.Claims = sub,auth_time,idp,amp, context.RequestedClaimTypes = sub
SturmUndDrang
  • 1,876
  • 5
  • 27
  • 47
  • The answers of [this question](https://stackoverflow.com/questions/53976553/identityserver4-role-based-authorization-for-web-api-with-asp-net-core-identity) may actually answer your question. –  Dec 04 '19 at 20:56

5 Answers5

1

As I'm not seeing what happens in your await AssembleUserClaims(context); I would suggest to check if it is doing the following:

Based on the the access token that you have from either the context.ProtoclMessage.AccessToken or from the call to the TokenEndpoint you should create a new ClaimsIdentity. Are you doing this, because you are not mentioning it?

Something like this:

var tokenClient = new TokenClient(
                      IdentityServerTokenEndpoint,
                      "clientId",
                      "clientSecret");


var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
                        n.Code, n.RedirectUri);

if (tokenResponse.IsError)
{
    throw new Exception(tokenResponse.Error);
}

// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);

id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
id.AddClaims(n.AuthenticationTicket.Identity.Claims);

// get user info claims and add them to the identity
var userInfoClient = new UserInfoClient(IdentityServerUserInfoEndpoint);
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
var userInfoEndpointClaims = userInfoResponse.Claims;

// this line prevents claims duplication and also depends on the IdentityModel library version. It is a bit different for >v2.0
id.AddClaims(userInfoEndpointClaims.Where(c => id.Claims.Any(idc => idc.Type == c.Type && idc.Value == c.Value) == false));

// create the authentication ticket
n.AuthenticationTicket = new AuthenticationTicket(
                        new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
                        n.AuthenticationTicket.Properties);

And one more thing - read this regarding the resources. In your particular case, you care about IdentityResources (but I see that you also have it there).

So - when calling the UserInfoEndpoint do you see the claims in the response? If no - then the problem is that they are not issued.

Check these, and we can dig in more.

Good luck

EDIT

I have a solution that you may, or may not like, but I'll suggest it.

In the IdentityServer project, in the AccountController.cs there is a method public async Task<IActionResult> Login(LoginInputModel model, string button).

This is the method after the user has clicked the login button on the login page (or whatever custom page you have there).

In this method there is a call await HttpContext.SignInAsync. This call accept parameters the user subject, username, authentication properties and list of claims. Here you can add your custom claim, and then it will appear when you call the userinfo endpoint in the AuthorizationCodeReceived. I just tested this and it works.

Actually I figured out that this is the way to add custom claims. Otherwise - IdentityServer doesn't know about your custom claims, and is not able to populate them with values. Try it out and see if it works for you.

m3n7alsnak3
  • 3,026
  • 1
  • 15
  • 24
  • I have updated the question to include the missing code. I don't see the claims in the response from UserInfoEndpoint even though I am adding them explicitly in the ProfileService (context.IssuedClaims). I've also moved it into from SecurityTokenValidated to AuthorizationCodeReceived – SturmUndDrang Mar 14 '18 at 16:47
  • So then the problem is when issuing them. In the `GetProfileDataAsync` in the `context.Subject.Claims` what do you have? Do you see the claims there? – m3n7alsnak3 Mar 14 '18 at 18:28
  • I've added some more detail in the question explaining what's going on at different stages of the process – SturmUndDrang Mar 14 '18 at 22:02
  • I noticed one thing - in your Identity Server Client configuration set the `AlwaysIncludeUserClaimsInIdToken` to true (it defaults to false) – m3n7alsnak3 Mar 14 '18 at 22:09
  • I've tried that as well. Unfortunately I couldn't see a difference :( – SturmUndDrang Mar 14 '18 at 22:15
  • what happens if in the `GetProfileDataAsync` you just say `context.IssuedClaims = context.Subject.Claims.ToList();` ? It sounds a bit hacky, but let's see what happens – m3n7alsnak3 Mar 14 '18 at 22:16
  • Sorry, I've tried setting AlwaysSendClientClaims to true, but not AlwaysIncludeUserClaimsInIdToken. The claims seem to be OK in the Id token though, it's the access token that doesn't have what I expect. If I set the issuedclaims to the context.subject.claims then when I call the UserInfoEndpoint i'll only get the "sub" claim back – SturmUndDrang Mar 14 '18 at 22:22
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/166848/discussion-between-m3n7alsnak3-and-sturmunddrang). – m3n7alsnak3 Mar 14 '18 at 22:27
  • No not yet. I'm giving up on hybrid for the moment (I'm going to use implicit+client credentials), unless something comes up here. Not sure what else I could try :( – SturmUndDrang Mar 17 '18 at 20:14
  • I am already doing this, and it adds all the users resource claims as well as identity claims. This doesn't seem to affect the auth code/access token though – SturmUndDrang Mar 17 '18 at 20:26
  • See the parts I've labelled with "all userclaims" in the question - these are all the claims that I add in from the Signin method in the logincontroller – SturmUndDrang Mar 17 '18 at 20:33
0

You need to modify the code of "Notifications" block in MVC App like mentioned below:

 Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthorizationCodeReceived = async n => {
                        var userInfoClient = new UserInfoClient(UserInfoEndpoint);
                        var userInfoResponse = await userInfoClient.GetAsync(n.ProtocolMessage.AccessToken);

                        var identity = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
                        identity.AddClaims(userInfoResponse.Claims);

                        var tokenClient = new TokenClient(TokenEndpoint, "portal", "secret");
                        var response = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);

                        identity.AddClaim(new Claim("access_token", response.AccessToken));
                        identity.AddClaim(new Claim("expires_at", DateTime.UtcNow.AddSeconds(response.ExpiresIn).ToLocalTime().ToString(CultureInfo.InvariantCulture)));
                        identity.AddClaim(new Claim("refresh_token", response.RefreshToken));
                        identity.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
                        n.AuthenticationTicket = new AuthenticationTicket(identity, n.AuthenticationTicket.Properties);

                    },
                    RedirectToIdentityProvider = n =>
                    {
                        if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
                        {
                            var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token").Value;
                            n.ProtocolMessage.IdTokenHint = idTokenHint;
                        }

                        return Task.FromResult(0);
                    }
                }

(consider if any changes related to the version of identity server as this code was built for identity server 3.)

Riddhi
  • 201
  • 1
  • 10
  • I am doing the above but in SecurityTokenValidated instead of AuthorizationCodeReceived. When I put it in AuthorizationCodeReceived my MVC controller continues to execute before the token is returned and so I get errors (when it tries to access the claims principal). The problem I have is that ApiResources are being ignored on code requests and they are needed so that the access token can assemble the correct claims – SturmUndDrang Mar 14 '18 at 13:23
0

Why do you have "portal" listed as an identity resource and Api resource? That could be causing some confusion.

Also, before I switched to IdentityServer4 and asp.net core, my IdentityServer3 startup code looked very similar to what you have with MVC. You may want to look at the examples for IdentityServer3.

Some suggestions I may give, in your "ResponseType" field for MVC, you could try "code id_token token"

Also, you are setting your claims on AuthorizationCodeReceived, instead use SecurityTokenValidated.

But you shouldn't have to do anything custom like people are mentioning. IdentityServer4 handles custom ApiResources like you are attempting to do.

j-hurst
  • 289
  • 1
  • 3
  • 12
  • I have definitely been there many a times trying to figure this stuff out. It is a great open source tool but can be confusing at times. I could potentially try mocking something up a little later. My guess is you are using the 4.6.* MVC? – j-hurst Mar 20 '18 at 18:45
  • I'm using MVC 5.2.4.0. I've got different names in the api and identity resources in my actual code (i named them to remove our product name), i'll update the question so that they're different. Same with adding token to the response type. I have gone back to setting the claims in SecurityTokenValidated (that's what I tried originally). Thanks for your help – SturmUndDrang Mar 20 '18 at 21:31
-1

You can try to implement your own IProfileService and override it following way:

services.AddIdentityServer()
    .//add clients, scopes,resources here
    .AddProfileService<YourOwnProfileProvider>();

For more information look up here:

https://damienbod.com/2016/10/01/identityserver4-webapi-and-angular2-in-a-single-asp-net-core-project/

Oleksandr Nahirniak
  • 1,357
  • 2
  • 9
  • 12
-1
  1. portal is not an identity resource: you should remove

new IdentityResource("portal", new List{ "tenantId", "userId", "user", "role", "name"})

  1. Names for the api resources should be consistent:

    public static IEnumerable GetApiResources()
    {
        return new List
            {
                new ApiResource
                {
                    Name= "portal",
                    UserClaims = { "tenantId","userId","user" }, 
                    Scopes =
                    {
                        new Scope("portal","portal")
                    }
                },
    
        };
    }
    
    1. Try setting GrantTypes.Implicit in the client.
Vadim Lopatkin
  • 134
  • 1
  • 7