fireproofsocks
Using Joken to validate Google JWTs
I’ve been trying out the Joken package to work with JWTs. Right now I’m unable to verify the JWTs that Google generates during Oauth. Has anyone implemented this? I think Joken has all the pieces, I just can’t figure out the winning combination. I’ve been reading this: https://developers.google.com/identity/sign-in/web/backend-auth
Google’s public keys are available in JWK format https://www.googleapis.com/oauth2/v3/certs
and in PEM format: https://www.googleapis.com/oauth2/v1/certs
I can get Google JWT, and I’ve tried to set up a module for these particular Google JWTs:
defmodule GoogleJwt do
use Joken.Config, default_signer: nil # no signer
def token_config do
default_claims()
end
end
But the following always generates an error of invalid signature:
MyGoogleJwt.verify_and_validate(idtoken)
Anyone have any tips?
Most Liked
vinagrito1
Heya guys. So I’m another soul that struggled with something that was supposed to be much easier for a while. And finally landed here.
So after trying all the libraries out there I’m sticking to JOSE. Thx @dom for mentioning it. As @fireproofsocks pointed out previously
I can run something like:
token = "from google oauth"
pem = "--- relevant key copied from https://www.googleapis.com/oauth2/v1/certs"
jwk = JOSE.JWK.from_pem(pem)
JOSE.JWT.verify_strict(jwk, ["RS256"], token)
Will do the job. Thank you all for the inputs
victorolinasc
Sorry for such a delayed response. I’ve replied @fireproofsocks on Joken repo about RS keys configuration and released a more detailed configuration of asymmetric keys guide.
To validate tokens from Google, Microsoft and other that have an OpenID Connect certs endpoint (or similar) with a published JWKS, one can use JokenJwks.
You’d do simply:
defmodule MyGoogleToken do
use Joken.Config
add_hook(JokenJwks, strategy: MyGoogleStrategy)
def token_config do
# your config here with what you want to validate from the token
end
end
defmodule MyGoogleJwksStrategy do
use JokenJwks.DefaultStrategyTemplate
def init_opts(opts), do: [jwks_url: "https://www.googleapis.com/oauth2/v1/certs"]
end
defmodule MyApp do
use Application
def start(_type, _args) do
children =
[
MyGoogleStrategy,
# others
]
# other start logic
end
spencerdcarlson
If you are just interested in using Google’s certs to validate a JWT issued by them, I made the google_certs hex package.
The Google Certs package will download, cache, and auto refresh Google’s certs. Both v1 PEM format and v3 JWK format are supported and are ready to work with Joken (see GoogleCerts.fetch/1). Here is the example from the how to use with joken section in the docs:
defmodule MyApp.Application do
@moduledoc false
use Application
alias GoogleCerts.CertificateCache
def start(_type, _args) do
children = [
CertificateCache
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
defmodule MyApp.Crypto.VerifyHook do
@moduledoc false
use Joken.Hooks
@impl true
def before_verify(_options, {jwt, %Joken.Signer{} = _signer}) do
with {:ok, %{"kid" => kid}} <- Joken.peek_header(jwt),
{:ok, algorithm, key} <- GoogleCerts.fetch(kid) do
{:cont, {jwt, Joken.Signer.create(algorithm, key)}}
else
error -> {:halt, {:error, :no_signer}}
end
end
end
defmodule MyApp.Crypto.JWTManager do
@moduledoc false
use Joken.Config, default_signer: nil
@iss "https://accounts.google.com"
# your google client id (usually ends in *.apps.googleusercontent.com)
defp aud, do: Application.get_env(:my_app, :google_client_id)
# reference your custom verify hook here
add_hook(MyApp.Crypto.VerifyHook)
@impl Joken.Config
def token_config do
default_claims(skip: [:aud, :iss])
|> add_claim("iss", nil, &(&1 == @iss))
|> add_claim("aud", nil, &(&1 == aud()))
end
end
# anywhere in your app you can verify and validate a Google issued JWT
iex> jwt = "eyJhbGciOiJSUzI1..." # Google issued JWT (api call, uberauth, etc)
iex> {:ok, claims} = JWTManager.verify_and_validate(jwt)
easco
I think your first mistake is using the mechanism where you use Joken.Config to create a custom module for handling tokens. In this case you want to handle someone else’s tokens so I’m not sure what you’re doing is the right strategy. But more on that later.
You’re going to have to do something to get a Joken Signer based on the Google keys.
In my application I have the signing keys specified as JWK maps. I create the Joken Signer using:
signer = Joken.Signer.create(signing_key["alg"], signing_key)
I then verify the token as:
Joken.verify(iam_token, signer, [])
This lets me know that the claims in the token were signed by the signer which is all I want to know.
If you want to validate that the verified claims are good, then you woud create a token_config() with the validations added. Riffing on an example in the docs it might look something like:
token_config = %{} # empty claim map
|> add_claim("name", nil, &(&1 == "John Doe")) # name has to be John Doe
|> add_claim("test", nil, &(&1 == true)) # test has to be true
|> add_claim("age", nil, &(&1 > 18)) # age must be > 18
Then you could pass the token_config to Joken.verify_and_validate
Joken.verify_and_validate(token_config, your_token, google_signer)
If you were going to make the mechanism you have above work (with the use Joken.Config), you would have to provide a default_signer. Joken expects you to specify the default_signer in your application’s config.exs. It wants you to provide the signer that your app would use to sign its own tokens. You might be able to do that grab Google’s the signing keys then, query the web when config.exs is compiled, but if the keys changed you’d be in trouble.
(you would also implement a token_config function in your module to return something like the token_config constructed in the sample above)
You might be able to create a function that grabs the signature at runtime and jams the default signer into the application config using Application.put_env - something along the lines of Application.put_env(:your_app_key, GoogleJWT, default_signer: signer). You’d need a strategy to update that signer (either periodically, or when a verification fails… something like that).
dom
YMMV, but I found it much easier to validate tokens using the JOSE library directly than with Jokens. The errors contain more information on what failed, and the code is much easier to follow (no macros, overrides, callbacks).







