Skip to content

Support verification of HS256-signed JWTs #134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 22, 2023
Merged

Support verification of HS256-signed JWTs #134

merged 2 commits into from
Jan 22, 2023

Conversation

stanhu
Copy link
Contributor

@stanhu stanhu commented Nov 21, 2022

When an OpenID provider such as Keycloak is configured to use the
HS256 algorithm to sign JSON Web Tokens (JWTs), previously logins
would fail with an obscure error:

JSON::JWS::UnexpectedAlgorithm (no implicit conversion of OpenSSL::PKey::RSA into String)

The HS256 algorithm relies on a shared secret between the client and
the server, and this secret is not in the list of JSON Web Key Set
(JWKS) that is typically retrieved via a public endpoint during OpenID
Discovery.

The error was happening because decoding a HS256-signed JWT with
public key RSA key will fail. We should only attempt to decode with
the configured client_options.secret.

To do this, we need to decode the JWT to examine the header to
determine how it was signed. For example:

{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "RrHQomcBaw2FJZ9Q5skkNaPC6sHJFLUAf4uoeaItDPE"
}

This indicates that the payload was signed with RS256, a public key
algorithm.

Once we know the algorithm used to decode, we can determine which key
to use. For public key algorithms such as RS256, we use the
JWKS. For signature-based algoriths such as HS256, we use the
configured client secret.

This merge request also cleans up some technical debt. Previously we
didn't peek at the unverified JWT to determine the key ID (kid) that
the payload was signed with. If we got a KidNotFound exception from
the decoding, previously we couldn't tell whether this was happening
because we didn't have a matching key in JWKS, or whether the JWT
didn't have a kid to begin with. Now that we decode the header, we
can tell these cases apart. If the JWT has no kid and we get
KidNotFound from decoding, we know the server didn't supply the
right set of keys. Otherwise, we can just use try key until we find
one that works.

Relates to
https://gitlab.com/gitlab-org/ruby/gems/gitlab-omniauth-openid-connect/-/issues/1

This is part of the effort to upstream changes in the GitLab fork:
https://gitlab.com/gitlab-org/ruby/gems/gitlab-omniauth-openid-connect/-/issues/5.

@stanhu stanhu force-pushed the sh-fix-hs256-jwt branch 2 times, most recently from 540aadc to 5dabd6f Compare November 21, 2022 04:16
When an OpenID provider such as Keycloak is configured to use the
HS256 algorithm to sign JSON Web Tokens (JWTs), previously logins
would fail with an obscure error:

```
JSON::JWS::UnexpectedAlgorithm (no implicit conversion of OpenSSL::PKey::RSA into String)
```

The HS256 algorithm relies on a shared secret between the client and
the server, and this secret is not in the list of JSON Web Key Set
(JWKS) that is typically retrieved via a public endpoint during OpenID
Discovery.

The error was happening because decoding a HS256-signed JWT with
public key RSA key will fail. We should only attempt to decode with
the configured `client_options.secret`.

To do this, we need to decode the JWT to examine the header to
determine how it was signed. For example:

```json
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "RrHQomcBaw2FJZ9Q5skkNaPC6sHJFLUAf4uoeaItDPE"
}
```

This indicates that the payload was signed with `RS256`, a public key
algorithm.

Once we know the algorithm used to decode, we can determine which key
to use. For public key algorithms such as `RS256`, we use the
JWKS. For signature-based algoriths such as `HS256`, we use the
configured client secret.

This merge request also cleans up some technical debt. Previously we
didn't peek at the unverified JWT to determine the key ID (`kid`) that
the payload was signed with. If we got a `KidNotFound` exception from
the decoding, previously we couldn't tell whether this was happening
because we didn't have a matching key in JWKS, or whether the JWT
didn't have a `kid` to begin with. Now that we decode the header, we
can tell these cases apart. If the JWT has no `kid` and we get
`KidNotFound` from decoding, we know the server didn't supply the
right set of keys. Otherwise, we can just use try key until we find
one that works.

Relates to
https://gitlab.com/gitlab-org/ruby/gems/gitlab-omniauth-openid-connect/-/issues/1

This is part of the effort to upstream changes in the GitLab fork:
https://gitlab.com/gitlab-org/ruby/gems/gitlab-omniauth-openid-connect/-/issues/5.
If a client is configured for a specific `client_signing_alg` but the
JWT returns a different algorithm, fail the callback immediately to
prevent some insecure downgrade from happening.
@stanhu stanhu merged commit b6ef0f8 into master Jan 22, 2023
@stanhu stanhu deleted the sh-fix-hs256-jwt branch January 22, 2023 05:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant