panva / openid-client

OAuth 2 / OpenID Connect Client API for JavaScript Runtimes
MIT License
1.83k stars 392 forks source link

id_token verify JWT Using HMAC SHA-256 (HS256, HS384, or HS512) #315

Closed gtorresani closed 3 years ago

gtorresani commented 3 years ago

Describe the bug

When using the HS256 algorithm to sign the JWT according to the specifics of OpenId-Connect, the signing key must be the octets of the UTF-8 representation of client_secret. However the client_secret is currently base64url encoded.

https://openid.net/specs/openid-connect-core-1_0-13.html#IDTokenValidation

  1. If the alg parameter of the JWT header is a MAC based algorithm such as HS256, HS384, or HS512, the octets of the UTF-8 representation of the client_secret corresponding to the client_id contained in the aud (audience) Claim are used as the key to validate the signature. Multiple audiences are not supported for MAC based algorithms.

To Reproduce I have used the example provided in RFC 7515, JWS Using HMAC SHA-256.

OctKey

{
  "kty":"oct",
  "k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
}

JWT

eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
const jose = require('jose');
const base64url = require('base64url');

const jwt = 'eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9'
  + '.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ'
  + '.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk';
const clientSecret = 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow';

const key = jose.JWK.asKey({ k: base64url.encode(clientSecret), kty: 'oct' });
const result = jose.JWS.verify(jwt, key, { complete: true });
console.log(result);

Current behavior results

/node_modules/jose/lib/jws/verify.js:159
      throw new errors.JWSVerificationFailed()
      ^

JWSVerificationFailed: signature verification failed
    at jwsVerify (/Users/gtorresa/Documents/workspace/mda-api/node_modules/jose/lib/jws/verify.js:159:13)
    at Object.<anonymous> (...)
    at Module._compile (internal/modules/cjs/loader.js:778:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:831:12)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:623:3)

Currently, the joseSecret function creates a JWK based on the base64UrlEncode representation of client_secrete, while the OpenId-Connect standard requires that it be the octets of the UTF-8 representation of client_secret. Otherwise it is not necessary to perform a base64url encoding.

Expected behaviour

const jose = require('jose');
const base64url = require('base64url');

const jwt = 'eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9'
  + '.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ'
  + '.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk';
const clientSecret = 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow';

const key = jose.JWK.asKey({ k: clientSecret, kty: 'oct' });
const result = jose.JWS.verify(jwt, key, { complete: true });
console.log(result);
{
  "payload": {
    "iss": "joe",
    "exp": 1300819380,
    "http://example.com/is_root": true
  },
  "key": {
    "kid": "y_x3gCJnL6oKGBBIXScabduwxTVy2Wd2bzRVEUbdUzc",
    "kty": "oct"
  },
  "protected": {
    "typ": "JWT",
    "alg": "HS256"
  }
}

Environment:

Additional context

panva commented 3 years ago

However the client_secret is currently base64url encoded.

client_secret values are not base64url encoded, they are the actual utf-8 octets to use. The behaviour exhibited by openid-client is correct and has been confirmed with the openid foundation conformance software as well as years of actual usage against a number of identity providers.

Only when representing a client_secret as an oct JWK needs the value of the client_secret be additionally base64url encoded, that comes from the JWK specification.

gtorresani commented 3 years ago

Thank you for your quick response

Indeed I find the results of the tests however no test is carried out with the HS256 algorithm, only the RS256 algorithm is tested during the certification.

Otherwise why the JWT in the example of RFC 7515 does not work by performing a base64UrlEncode of client_secret while it works without base64UrlEncode.

panva commented 3 years ago

I'm not sure how to properly decode your answer and the issue, there's a lot mixup in there, so let's try differently.

What is the client_secret value given to you by your IdP? (generate a new client for this, don't post your actual one).

panva commented 3 years ago

The secret value in the JWK you posted is not a valid client_secret value, it containts invalid char codes. That's why you can't pass just the base64url decoded string value of the "k" property as the secret, because no idp will give you such value as a client_secret

panva commented 3 years ago

Currently, the joseSecret function creates a JWK based on the base64UrlEncode representation of client_secrete, while the OpenId-Connect standard requires that it be the octets of the UTF-8 representation of client_secret. Otherwise it is not necessary to perform a base64url encoding.

The function returns an oct JWK representation of the client_secret. In which the octets of a string will be base64url encoded otherwise it's not encoded as a JWK properly, the underlying crypto will decode the k again to end up with a non-encoded secret. Passing in just the string would yield exactly the same results.

gtorresani commented 3 years ago

Thanks for those details.

So if my client_secret is aBCD1234,;:-= the associated OctKey is

{
   "kid": "cWzWbpYC9nKIbXmQMXk2CHfjdu28DENLjLjS-fk93gc",
   "kty": "oct",
   "k": "YUJDRDEyMzQsOzotPQ"
}

In this case everything works to decode the JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.BOy50CByAmMGzdDnG_vsRm7BuF5uvdKycTc0zVWzHxA

Thank you for your responsiveness and sorry for my unnecessary request.

panva commented 3 years ago

You got it.

Please consider supporting the library if it provides value to you or your company and this support was of help to you. Supporting the library means, amongst other things, that such support will be available in the future.