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:
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.
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:
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:
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 asHS256
, 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 aKidNotFound
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 akid
to begin with. Now that we decode the header, we can tell these cases apart. If the JWT has nokid
and we getKidNotFound
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.