fireproofsocks

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

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

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

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

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

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).

Where Next?

Popular in Questions Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
Codball
Mix format works fine if run from the cmd. I’ve followed this to facilitate the implementation into VSC which involves downloading an ext...
New

Other popular topics Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

We're in Beta

About us Mission Statement