Validating RS256-signed JSON Web Tokens in Erlang
I’m currently playing with OpenID Connect (OAuth 2.0 for Login), to allow people to log into a web site using their Google account. The web site is built using Erlang.
By following the Google documentation, I’ve managed to send an authentication request to Google, exchange the code returned for an ID token, and extract the user information from the ID token.
The documentation says:
since you are communicating directly with Google over an intermediary-free HTTPS channel and using your client secret to authenticate yourself to Google, you can be confident that the token you receive really comes from Google and is valid.
However, I wanted to deal with the token validation as well. Sample code for C#, Java, Ruby etc. exists, but I couldn’t find anything for Erlang, so…
ID Token
If you use Google OpenID Connect, you get back an id_token that looks
something like eyJh…MifQ.eyJp…MzR9.rG-s…btCc.
This is a JSON Web Token (JWT).
It’s in three parts (header, payload and signature), separated by dots. Each part is base64url-encoded. Erlang doesn’t have a built-in base64url module, so you’ll have to find one.
[H, P, S] = binary:split(Token, <<".">>, [global]).
Header = jiffy:decode(base64url:decode(H), [return_maps]).
Payload = jiffy:decode(base64url:decode(P), [return_maps]).
Signature = base64url:decode(S).
Key ID
The header is JSON, and looks something like this:
{
"alg": "RS256",
"kid": "8faca3e0eff37d416d0a8a9770d8f09c7eeffce3"
}
The signing algorithm is given is given in the header as RS256, which is “RSA using SHA-256 hash algorithm”. The
particular certificate to be used is specified by the kid field.
#{<<"kid">> := KId} = Header.
Google’s OpenID Configuration
To find Google’s JWT signing certificates, we first need to get the Discovery document for Google’s OpenID Connect
service, which is at https://accounts.google.com/.well-known/openid-configuration:
{ok, _} = application:ensure_all_started(inets).
{ok, _} = application:ensure_all_started(ssl).
ConfigurationUrl =
"https://accounts.google.com/.well-known/openid-configuration".
{ok, { {_, 200, _}, _, ConfigurationJson}} =
httpc:request(ConfigurationUrl).
Configuration = jiffy:decode(ConfigurationJson, [return_maps]).
Google Signing Keys
The discovery document contains a field jwks_uri which points to the JWT signing keys:
#{<<"jwks_uri">> := JwksUri} = Configuration.
And we can go and get that document:
{ok, { {_, 200, _}, _, JwksJson}} =
httpc:request(binary_to_list(JwksUri)).
This returns a JSON object that looks like the following:
{
"keys": [
{
"kty": "RSA", "alg": "RS256", "use": "sig",
"kid": "8faca3e0eff37d416d0a8a9770d8f09c7eeffce3",
"n": "xJiA...5Kik", "e": "AQAB"
},
...
]
}
The real document has more than one key in it. You should look up the entry where the kid field matches the one
specified in the token.
According to the Google documentation, we should cache this document. The Cache-Control and Expires HTTP headers
appear to be set appropriately for this.
Jwks = jiffy:decode(JwksJson, [return_maps]).
#{<<"keys">> := Keys} = Jwks.
[Key] = lists:filter(
fun(Key) ->
#{<<"kid">> := K} = Key,
K =:= KId
end, Keys).
Public Key Modulus and Exponent
We need the public key modulus and exponent, which are n and e respectively. They’re base64url-encoded, and we want
them as integers, so:
#{<<"n">> := N0, <<"e">> := E0} = Key.
N1 = base64url:decode(N0).
E1 = base64url:decode(E0).
N = binary:decode_unsigned(N1).
E = binary:decode_unsigned(E1).
Validating the signature
Msg = iolist_to_binary([H, <<".">>, P]),
IsValid = crypto:verify(rsa, sha256, Msg, Signature, [E, N]).
Done?
Not quite. The documentation requires 5 steps:
Verify that the ID token is a JWT which is properly signed with an appropriate Google public key.- Verify that the value of
audin the ID token is equal to your app’s client ID. - Verify that the value of
issin the ID token is equal toaccounts.google.comorhttps://accounts.google.com. - Verify that the expiry time (
exp) of the ID token has not passed. - If you passed a
hdparameter in the request, verify that the ID token has ahdclaim that matches your Google Apps hosted domain.
We’ve only done the first step, but it is the hardest. The others are left as an exercise for the reader.