I'm using the Dart JsonWebToken Library https://pub.dev/packages/dart_jsonwebtoken to generate a client assertion JWT for a OAuth2 client credential grant.
The JWT seems to generate correctly and is signed correctly according to JWT.IO, tested with public and private keys.
However, when submitted with the client credential grant in the client_assertion field it returns an invalid token signature.
Error:
"invalid_client" "Invalid token signature"
I have tested using KJUR library with postman and with the same keys, claims and algorythim using in the request, it works. So I'm calling on help from the community to point me in the right direction.
Code Example: (Dart - Invalid Signature)
Future<String> _generateJWT() async {
final current_time = DateTime.now();
final expiration_time = current_time.add(Duration(minutes: 50));
final issued_time = current_time;
final uuid = Uuid();
final jwt = JWT(
header: Map.from({
'alg': 'PS512',
'kid': 'kid1234546',
'typ': 'JWT',
}),
{
'sub': clientID,
'aud':
'https://provider.com/oauth/v2.0/token',
'iss': clientID,
'exp': (expiration_time.millisecondsSinceEpoch ~/ 1000),
'iat': (issued_time.millisecondsSinceEpoch ~/ 1000) - 120,
'jti': uuid.v4(),
},
);
final token = jwt.sign(
RSAPrivateKey(privateKey),
algorithm: JWTAlgorithm.PS512,
);
return token;
}
code example: Working (Postman/KJUR)
const clientAssertionJwt = utils.generateSignedRequestJWT(
KJUR.jws.JWS.sign,
{
alg: 'PS512',
kid,
typ: 'JWT',
},
{
sub: clientId,
aud,
iss: clientId,
exp: Math.floor(Date.now() / 1000) + 3590,
iat: Math.floor(Date.now() / 1000) - 120,
jti: v4(),
},
privateKey,
);
LIbrary Util Function
const generateSignedRequestJWT = (sign, header, payload, privateKey) => {
if (!header || !payload || !privateKey) {
throw new Error(
`utils.generateSignedRequestJWT - Missing required argument. Following arguments have been provided: header: ${!!header}, payload: ${!!payload}, privateKey: ${!!privateKey}`,
);
}
try {
// KJUR.jws.JWS.sign
return sign(
header.alg,
JSON.stringify(header),
JSON.stringify(payload),
privateKey,
);
} catch (err) {
console.error(
'utils.generateSignedRequestJWT - Failed to generate signed request JWT',
err,
{
header,
payload,
privateKeyDefined: !!privateKey,
},
);
return null;
}
};
This is a bug in the Dart library dart_jsonwebtoken. Besides PS512, PS384 does not work either. PS256 on the other hand works.
The reason for this is that the library uses a fixed salt length of 32 bytes for PSS, see here.
However, 32 bytes is incorrect for PS384 and PS512. RFC 7518 defines in chapter 3.5 Digital Signature with RSASSA-PSS:
The size of the salt value is the same size as the hash function output.
This means that for PS256 with SHA-256 as digest the salt length must be 32 bytes, for PS384 with SHA-384 as digest 48 bytes and for PS512 with SHA-512 as digest 64 bytes. Therefore PS256 works, but not PS384 and PS512.
This can also be checked by explicitly verifying the PS512 signature with RSA/PSS using e.g. PointyCastle and applying a salt length of 32 bytes. The verification is then successful.
You mention in your question:
The JWT seems to generate correctly and is signed correctly according to JWT.IO, tested with public and private keys.
Why the verification on jwt.io is successful for you is not clear to me. In my tests, tokens signed with PS512 that are generated with the Dart library cannot be verified successfully on jwt.io, as expected. You may have used PS256 or you may have accidentally re-initiated signing for PS512 with the default keys, overwriting the old signature with a new one. But these are just guesses.
Apparently you have already filed an issue (#58).
I cannot confirm that the signed token generated with KJUR has a different length than the one generated with dart_jsonwebtoken. In both cases, a header and a payload with the following claims are generated:
{
"alg": "PS512",
"kid": "kid1234546",
"typ": "JWT"
}
{
"sub": "some client ID",
"aud": "https://provider.com/oauth/v2.0/token",
"iss": "some client ID",
"exp": 1731498111,
"iat": 1731494401,
"jti": "a72f8e08-c2ae-463c-96c0-f32fd7c8c6c6"
}
and a signature size that (after Base64url decoding) corresponds to the key size used.
Maybe you applied different key sizes or both codes generate different claims/values in your environment (for some reason). If you cannot find the cause, you can edit your question and post examples of the signed tokens generated with both libraries. Otherwise, I recommend to revise the submitted issue so that the description does not distract from the actual problem.