Skip to content

Commit

Permalink
if a "kid" is included in the jwk, pass it to the jws as a hint (#654)
Browse files Browse the repository at this point in the history
* if a "kid" is included in the jwk, pass it to the jws as a hint when decoding

* fix formatting

* add documentation snippet regarding the "kid" header

Co-authored-by: Matt Pinkston <[email protected]>
  • Loading branch information
mpinkston and mpinkston authored Jun 4, 2020
1 parent 694e546 commit 13815be
Show file tree
Hide file tree
Showing 2 changed files with 25 additions and 4 deletions.
25 changes: 22 additions & 3 deletions lib/guardian/token/jwt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ defmodule Guardian.Token.Jwt do
end
end
```
If the signing secret contains a "kid" (https://tools.ietf.org/html/rfc7515#section-4.1.4)
it will be passed along to the signature to provide a hint about which secret was used.
This can be useful for specifying which public key to use during verification if you're using
a public/private key rotation strategy.
An example implementation of this can be found here: [https://gist.github.com/mpinkston/469009001b694d3ca162894d74c9bfe3](https://gist.github.com/mpinkston/469009001b694d3ca162894d74c9bfe3)
"""

@behaviour Guardian.Token
Expand Down Expand Up @@ -253,10 +260,11 @@ defmodule Guardian.Token.Jwt do
def create_token(mod, claims, options \\ []) do
with {:ok, secret_fetcher} <- fetch_secret_fetcher(mod),
{:ok, secret} <- secret_fetcher.fetch_signing_secret(mod, options) do
jose_jwk = jose_jwk(secret)

{_, token} =
secret
|> jose_jwk()
|> JWT.sign(jose_jws(mod, options), claims)
jose_jwk
|> JWT.sign(jose_jws(mod, jose_jwk, options), claims)
|> JWS.compact()

{:ok, token}
Expand Down Expand Up @@ -386,6 +394,17 @@ defmodule Guardian.Token.Jwt do
end
end

# If the JWK includes a "kid" add this to the signature to provide a hint
# about which key was used.
# https://tools.ietf.org/html/rfc7515#section-4.1.4
defp jose_jws(mod, %JWK{fields: %{"kid" => kid}}, opts) do
header = %{"kid" => kid}
opts = Keyword.update(opts, :headers, header, &Map.merge(&1, header))
jose_jws(mod, opts)
end

defp jose_jws(mod, _, opts), do: jose_jws(mod, opts)

defp jose_jws(mod, opts) do
algos = fetch_allowed_algos(mod, opts) || @default_algos
headers = Keyword.get(opts, :headers, %{})
Expand Down
4 changes: 3 additions & 1 deletion test/guardian/token/jwt_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ defmodule Guardian.Token.JwtTest do
|> JOSE.JWS.compact()

es512_jose_jwk = JOSE.JWK.generate_key({:ec, :secp521r1})
es512_jose_jwk = JOSE.JWK.merge(es512_jose_jwk, %{"kid" => JOSE.JWK.thumbprint(es512_jose_jwk)})
es512_jose_jws = JOSE.JWS.from_map(%{"alg" => "ES512"})

es512_jose_jwt =
Expand Down Expand Up @@ -131,7 +132,8 @@ defmodule Guardian.Token.JwtTest do

{:ok, token} = Jwt.create_token(ctx.impl, ctx.claims, secret: secret, allowed_algos: ["ES512"])

{true, jwt, _} = JWT.verify_strict(secret, ["ES512"], token)
{true, jwt, jws} = JWT.verify_strict(secret, ["ES512"], token)
assert jws.fields["kid"] == secret.fields["kid"]
assert jwt.fields == ctx.claims

{:error, _reason} = JWT.verify_strict(ctx.impl.config(:secret_key), ["HS512"], token)
Expand Down

0 comments on commit 13815be

Please sign in to comment.