使用Dart JsonWebToken生成的Client Assertion JWT为何被IDP返回无效签名?
我用Dart的JsonWebToken库生成OAuth2客户端凭证授权所需的Client Assertion JWT,在JWT.IO上测试签名显示正常,但提交到IDP的client_assertion字段时,返回错误:
"invalid_client" "Invalid token signature"
用相同的密钥、声明和PS512算法,通过Postman中的KJUR库生成的JWT却能正常通过验证。以下是相关代码:
Dart 签名无效的代码示例
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; }
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, );
KJUR工具函数代码
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; } };
排查方向
密钥格式差异
检查Dart代码中传入的privateKey是否严格符合PKCS#8或PKCS#1格式,带正确的PEM头(-----BEGIN PRIVATE KEY-----)和尾,无多余换行/空格。KJUR可能自动兼容格式问题,但Dart库对格式要求更严格,可尝试转换密钥为PKCS#8格式后再传入。JSON序列化细节
对比两个库生成的JWT头部、载荷的Base64Url解码结果:- 检查字段顺序、空格处理是否完全一致。部分严格的IDP会依赖JSON序列化的字段顺序(尽管JWT标准无此要求),可在Dart中手动指定序列化字段顺序,对齐KJUR的输出。
时间戳时区问题
Dart的DateTime.now()默认是本地时间,转换的时间戳可能与KJUR用UTC时间生成的戳存在偏差。将Dart代码中的时间戳计算改为:final current_time = DateTime.now().toUtc();确保生成的
exp、iat为UTC时间戳,避免IDP因时间偏差拒绝验证。PS512算法参数差异
不同库对RSA-PSS(PS512)的盐长度默认值可能不同,KJUR默认盐长度与哈希长度一致(512位),可查看Dart库文档,手动指定盐长度为64字节(512位),对齐KJUR的算法参数。Base64Url编码细节
检查两个库生成的Base64Url是否严格遵循RFC 4648标准:确认+替换为-、/替换为_,且移除了末尾的=填充,避免编码细节差异导致签名验证失败。
内容的提问来源于stack exchange,提问作者Yonkee

