32

Using thinktecture JWT authentication resource owner flow, I use the claims part of JWT for client consumption.

My question is that if it's possible to add a claim in identity server and decode it as an array in client.

There is no ClaimTypeValues for array type.

As a workaround:

var user = IdentityServerPrincipal.Create(response.UserName, response.UserName);
user.Identities.First()
    .AddClaims(
        new List<Claim>()
        {
             new Claim(ClaimTypes.Name, response.UserName),
             new Claim(ClaimTypes.Email, response.Email),
             new Claim(FullName, response.FullName),
             new Claim(AuthorizedCompanies,JsonConvert.SerializeObject(response.AuthorizedCompanies))
        });

return new AuthenticateResult(user);

I add claim as json array to claim for AuthorizedCompanies and parse it in client side.What is the design pattern here if any ?

Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
sercan
  • 465
  • 1
  • 7
  • 13

5 Answers5

37

Speaking from personal experience, it is easier to inter-op with claim stores when the ValueType is always type "String". Although it may seem counter intuitive when you know you are dealing with a complex type, it is at least simple to understand.

The way I have approached this need for an array is to have my application code expect multiple claims to be present for the claim type in question, and keep each claim value of a simple type.

Examp:

var authorizeCompanies = identity.FindAll(AuthorizedCompanies).Select(c => c.Value);

And of course, you also add them that way:

identity.AddClaim(ClaimTypes.Name, response.UserName);
identity.AddClaim(AuthorizedCompanies, "CompanyX");
identity.AddClaim(AuthorizedCompanies, "CompanyY");
identity.AddClaim(AuthorizedCompanies, "CompanyZ");

IdentityServer supports this model out of the box. When generating a token for an identity such as this, it automatically writes the values for that claim out as an array.

{
    "aud": "Identity Server example/resources", 
    "iss": "Identity Server example", 
    "exp": 1417718816, 
    "sub": "1234",
    "scope": ["read", "write"], // <-- HERE
    "foo": ["bar", "baz"],      // <-- HERE TOO!
    "nbf": 1417632416
}

This approach to claims is in contrast to assuming all claims are a one-to-one mapping of type -> value.

Crescent Fresh
  • 115,249
  • 25
  • 154
  • 140
  • 2
    If you have only one *company* then you will not get an array, but a simple string in the *AuthorizedCompanies* claim. But if you have multiple, then you get an array of strings. Is this correct? – gajo357 Jul 03 '20 at 12:34
  • I was thinking of doing something like this, but then I found in the JWT specification this: _The Claim Names within a JWT Claims Set MUST be unique; JWT parsers MUST either reject JWTs with duplicate Claim Names or use a JSON parser that returns only the lexically last duplicate member name_ (https://tools.ietf.org/html/rfc7519 under Section 4) – FercoCQ Oct 17 '20 at 00:16
  • @FercoCQ I think IS is returning one claim name with the value of an json encoded array and thus it is not in a breach of the rfc definition. – Vočko Oct 27 '20 at 01:56
  • @Vočko indeed... I was looking at the problem from the .Net side, but I the code that actually builds the JWT must be converting the multiple "same type" claims into an array to comply with the specification. – FercoCQ Oct 27 '20 at 14:09
  • Note that the [JWT OAuth Access token spec](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-10#section-2.2.3,) says scope should formatted as described in RFC8693 which is a space delimited list not an array, so while you CAN do this you should not do it for scope. – Chris D Jan 14 '21 at 15:20
  • @FercoCQ I was in the same boat as you, thinking that this solution would breach the RFC definition, but the underlying code creates an array which is great! – Brendan Sluke Sep 23 '21 at 19:54
13

use JsonClaimValueTypes as claim type

var tokenDescriptor = new SecurityTokenDescriptor
   {
    Subject = new ClaimsIdentity(new Claim[]
     { new Claim("listName", list != null ? JsonSerializer.Serialize(user.RoleName) : string.Empty,JsonClaimValueTypes.JsonArray)
    }}
shahabas sageer
  • 171
  • 1
  • 12
7

I was having a similar issue, in my case I have claims that are arrays but sometimes only have one item depending on user permissions. In that case if you use new Claim("key", "value") to add them they will be strings when there is a single object and arrays when > 1 which was unacceptable.

A better solution in this case is to use JwtPayload to build the JwtSecurityToken object.

var payload = new JwtPayload
    {
        { "ver", version },
        { "iss", "example.com"},
        { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds()},
        { "exp", DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeSeconds()},
        { "aud", myExampleStringList }
    };
var token = new JwtSecurityToken(new JwtHeader(_signingCredentials), payload);

This works on .netcore 3.0 using System.IdentityModel.Tokens.Jwt v3.0 but I can't confirm for other versions.

  • This is also nice in case you want to control the order in which the claims appear – Thomas Sep 24 '21 at 07:55
  • `DateTimeOffset.UtcNow.ToUnixTimeSeconds()` returns the same number as `DateTimeOffset.Now.ToUnixTimeSeconds()`, so you should use the latter to save 3 keystrokes – Alexander Farber Jan 18 '23 at 16:26
  • @AlexanderFarber I don't think that's a good idea. You are probably seeing the same result from both methods because you are currently in a location having the UTC zone or your current system clock uses a UTC timezone. For most people, this would be different. – haey3 Aug 03 '23 at 21:36
  • Nope, I am in Germany. The number of seconds since Unix Epoch is same for all, regardless of the timezone. – Alexander Farber Aug 04 '23 at 11:18
2

Identity Server will convert the value from an array to string if you want to add a single value to an array. An easier solution would be to convert the array as json and add to the claim with valueType as json.

IList<string> companies = new List<string>();
companies.Add("CompanyA");
string companiesJson = JsonConvert.SerializeObject(companies);
context.IssuedClaims.Add(new Claim("Companies", companiesJson, IdentityServerConstants.ClaimValueTypes.Json));

The above solution will allow you to add one or more values to claim as an array.

Abhishek Dhotre
  • 481
  • 4
  • 17
2

I could finally fix the problem of converting a singleton array,

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Newtonsoft.Json;

IList<string> companyList = new List<string>();
companyList.Add("CompanyX");

string companiesJson = JsonConvert.SerializeObject(companyList);

claims.Add(new Claim("Companies", companiesJson, JsonClaimValueTypes.JsonArray));

var jwt = new JwtSecurityToken(
                issuer: ...,
                audience: ...,
                claims: claims ..);

This serializes the array of values as JSON and adds the claim as JSON Value Type.

Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
Nadun Kulatunge
  • 1,567
  • 2
  • 20
  • 28