Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add additional claims to be included in the access_token using ASP.Net Identity with IdentityServer4

How to add additional claims to be included within the token?

As soon as the API receives the bearer token, the User.Identity object gets populated with the following claims.

[
  {
    "key": "nbf",
    "value": "1484614344"
  },
  {
    "key": "exp",
    "value": "1484615244"
  },
  {
    "key": "iss",
    "value": "http://localhost:85"
  },
  {
    "key": "aud",
    "value": "http://localhost:85/resources"
  },
  {
    "key": "aud",
    "value": "WebAPI"
  },
  {
    "key": "client_id",
    "value": "MyClient"
  },
  {
    "key": "sub",
    "value": "d74c815a-7ed3-4671-b4e4-faceb0854bf6"
  },
  {
    "key": "auth_time",
    "value": "1484611732"
  },
  {
    "key": "idp",
    "value": "local"
  },
  {
    "key": "role",
    "value": "AccountsManager"
  },
  {
    "key": "scope",
    "value": "openid"
  },
  {
    "key": "scope",
    "value": "profile"
  },
  {
    "key": "scope",
    "value": "roles"
  },
  {
    "key": "scope",
    "value": "WebAPI"
  },
  {
    "key": "scope",
    "value": "offline_access"
  },
  {
    "key": "amr",
    "value": "pwd"
  }
]

I want additional claims like username, email, legacySystemUserId, etc. These fields already exist in the AspNetUsers table (and doesn't repetitively exist in AspNetUserClaims table) and are available in ASP .Net Core application in my ApplicationUser object.

I want them to be included in access token that is returned after authenticating with username and password. Want to use the same in my WebAPI application that doesn't have access to the identity-server database and its own database has data stored based on user's email address not the UserId (which is a guid generated in ASP .NET Identity and received as SUB claim).

like image 641
ravi punjwani Avatar asked Jan 17 '17 01:01

ravi punjwani


Video Answer


1 Answers

I had been fighting this same issue for hours and finally pieced together the solution. This article was a big help, but to summarize and share my implementation:

In order to get the claims assigned to the user and attach them to the access token, you need to implement two interfaces on the identity server: IResourceOwnerPasswordValidator and IProfileService. The following are my implementations of the two classes and are rough drafts, but they work.

**Be sure to get the latest version of IdentityServer4 - 1.0.2 at this time.

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
    private readonly UserManager<ApplicationUser> _userManager;

    public ResourceOwnerPasswordValidator(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
    {
        var userTask = _userManager.FindByNameAsync(context.UserName);
        var user = userTask.Result;

        context.Result = new GrantValidationResult(user.Id, "password", null, "local", null);
        return Task.FromResult(context.Result);
    }
}

and

public class AspNetIdentityProfileService : IProfileService
{
    private readonly UserManager<ApplicationUser> _userManager;

    public AspNetIdentityProfileService(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var subject = context.Subject;
        if (subject == null) throw new ArgumentNullException(nameof(context.Subject));

        var subjectId = subject.GetSubjectId();

        var user = await _userManager.FindByIdAsync(subjectId);
        if (user == null)
            throw new ArgumentException("Invalid subject identifier");

        var claims = await GetClaimsFromUser(user);

        var siteIdClaim = claims.SingleOrDefault(x => x.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
        context.IssuedClaims.Add(new Claim(JwtClaimTypes.Email, user.Email));
        context.IssuedClaims.Add(new Claim("siteid", siteIdClaim.Value));
        context.IssuedClaims.Add(new Claim(JwtClaimTypes.Role, "User"));

        var roleClaims = claims.Where(x => x.Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role");
        foreach (var roleClaim in roleClaims)
        {
            context.IssuedClaims.Add(new Claim(JwtClaimTypes.Role, roleClaim.Value));
        }
    }

    public async Task IsActiveAsync(IsActiveContext context)
    {
        var subject = context.Subject;
        if (subject == null) throw new ArgumentNullException(nameof(context.Subject));

        var subjectId = subject.GetSubjectId();
        var user = await _userManager.FindByIdAsync(subjectId);

        context.IsActive = false;

        if (user != null)
        {
            if (_userManager.SupportsUserSecurityStamp)
            {
                var security_stamp = subject.Claims.Where(c => c.Type == "security_stamp").Select(c => c.Value).SingleOrDefault();
                if (security_stamp != null)
                {
                    var db_security_stamp = await _userManager.GetSecurityStampAsync(user);
                    if (db_security_stamp != security_stamp)
                        return;
                }
            }

            context.IsActive =
                !user.LockoutEnabled ||
                !user.LockoutEnd.HasValue ||
                user.LockoutEnd <= DateTime.Now;
        }
    }

    private async Task<IEnumerable<Claim>> GetClaimsFromUser(ApplicationUser user)
    {
        var claims = new List<Claim>
        {
            new Claim(JwtClaimTypes.Subject, user.Id),
            new Claim(JwtClaimTypes.PreferredUserName, user.UserName)
        };

        if (_userManager.SupportsUserEmail)
        {
            claims.AddRange(new[]
            {
                new Claim(JwtClaimTypes.Email, user.Email),
                new Claim(JwtClaimTypes.EmailVerified, user.EmailConfirmed ? "true" : "false", ClaimValueTypes.Boolean)
            });
        }

        if (_userManager.SupportsUserPhoneNumber && !string.IsNullOrWhiteSpace(user.PhoneNumber))
        {
            claims.AddRange(new[]
            {
                new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber),
                new Claim(JwtClaimTypes.PhoneNumberVerified, user.PhoneNumberConfirmed ? "true" : "false", ClaimValueTypes.Boolean)
            });
        }

        if (_userManager.SupportsUserClaim)
        {
            claims.AddRange(await _userManager.GetClaimsAsync(user));
        }

        if (_userManager.SupportsUserRole)
        {
            var roles = await _userManager.GetRolesAsync(user);
            claims.AddRange(roles.Select(role => new Claim(JwtClaimTypes.Role, role)));
        }

        return claims;
    }
}

Once you have those, they need to be added to your services in startup.cs:

services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
services.AddTransient<IProfileService, AspNetIdentityProfileService>();

Here is a quick look at my config:

public static IEnumerable<IdentityResource> GetIdentityResources()
{
    return new List<IdentityResource>
    {
        new IdentityResources.OpenId()
    };
}

public static IEnumerable<ApiResource> GetApiResources()
{
    return new List<ApiResource>
    {
        new ApiResource
        {
            Name = "api1",
            Description = "My Api",
            Scopes =
            {
                new Scope()
                {
                    Name = "api1",
                    DisplayName = "Full access to Api"
                }
            }
        }
    };
}

public static IEnumerable<Client> GetClients()
{
    return new List<Client>
    {
        new Client
        {
            ClientId = "apiClient",
            ClientName = "Api Angular2 Client",
            AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
            AlwaysSendClientClaims = true,
            AlwaysIncludeUserClaimsInIdToken = true,
            ClientSecrets =
            {
                new Secret("secret".Sha256())
            },

            AllowedScopes =
            {
                "api1"
            }
        }
    };
}

After that, a call to the identity server from a client:

var discoTask = DiscoveryClient.GetAsync("http://localhost:5000");
var disco = discoTask.Result;

var tokenClient = new TokenClient(disco.TokenEndpoint, "apiClient", "secret");
var tokenResponseTask = tokenClient.RequestResourceOwnerPasswordAsync("[email protected]", "my-password", "api1");

var tokenResponse = tokenResponseTask.Result;
var accessToken = tokenResponse.AccessToken;

if (tokenResponse.IsError)
{
    Console.WriteLine(tokenResponse.Error);
    return;
}

Inspect the token at jwt.io and see your results...

like image 105
adova Avatar answered Oct 25 '22 12:10

adova