Add a claim to JWT as an array?
Asked Answered
D

5

33

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 ?

Devol answered 3/12, 2014 at 7:57 Comment(0)
K
40

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.

Kashmir answered 3/12, 2014 at 18:50 Comment(6)
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?Sibilate
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 (tools.ietf.org/html/rfc7519 under Section 4)Hoehne
@Hoehne 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.Libidinous
@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.Hoehne
Note that the JWT OAuth Access token spec 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.Shedd
@Hoehne 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!Mikaelamikal
K
20

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)
    }}
Kier answered 9/6, 2020 at 13:46 Comment(1)
Thank you, this really solved my problemLalla
S
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.

Shaquitashara answered 24/10, 2019 at 19:44 Comment(5)
This is also nice in case you want to control the order in which the claims appearBoo
DateTimeOffset.UtcNow.ToUnixTimeSeconds() returns the same number as DateTimeOffset.Now.ToUnixTimeSeconds(), so you should use the latter to save 3 keystrokesInadmissible
@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.Soiree
Nope, I am in Germany. The number of seconds since Unix Epoch is same for all, regardless of the timezone.Inadmissible
@AlexanderFarber According to the documentation, .Now is converted to UTC, and then the number of seconds is calculated. So, the result is always UTC, but Now.ToUnixTimeSeconds introduces a couple of extra conversions (from utc to local, from local to utc), whereas UtcNow.ToUnixTimeSeconds, doesn't require any conversions.Varro
V
3

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.

Vanegas answered 20/3, 2020 at 17:43 Comment(0)
R
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.

Rodenhouse answered 10/7, 2022 at 15:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.