Skip to content

Commit d81f868

Browse files
committed
Initial commit
0 parents  commit d81f868

10 files changed

+223
-0
lines changed

.formatter.exs

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

.gitignore

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
ex_firebase_auth-*.tar
24+
25+
26+
# Temporary files for e.g. tests
27+
/tmp
28+
29+
# elixir language server
30+
.elixir_ls/

README.md

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# ExFirebaseAuth
2+
3+
ExFirebaseAuth is a library that handles ID tokens from Firebase, which is useful for using Firebase's auth solution because Firebase does not have an Elixir SDK for auth themselves.
4+
5+
## Installation
6+
7+
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
8+
by adding `ex_firebase_auth` to your list of dependencies in `mix.exs`:
9+
10+
```elixir
11+
def deps do
12+
[
13+
{:ex_firebase_auth, "~> 0.1.0"}
14+
]
15+
end
16+
```
17+
18+
And add the Firebase auth issuer name for your project to your `config.exs`.
19+
20+
```elixir
21+
config :ex_firebase_auth, :issuer, "https://securetoken.google.com/hoody-16c66"
22+
```
23+
24+
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
25+
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
26+
be found at [https://hexdocs.pm/ex_firebase_auth](https://hexdocs.pm/ex_firebase_auth).

lib/ex_firebase_auth.ex

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
defmodule ExFirebaseAuth do
2+
# See http://elixir-lang.org/docs/stable/elixir/Application.html
3+
# for more information on OTP Applications
4+
@moduledoc false
5+
6+
use Application
7+
8+
def start(_type, _args) do
9+
import Supervisor.Spec, warn: false
10+
11+
# Define workers and child supervisors to be supervised
12+
children = [
13+
{Finch, name: ExFirebaseAuthFinch},
14+
{ExFirebaseAuth.KeyStore, name: ExFirebaseAuth.KeyStore}
15+
]
16+
17+
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
18+
# for other strategies and supported options
19+
opts = [strategy: :one_for_one, name: ExFirebaseAuth.Supervisor]
20+
Supervisor.start_link(children, opts)
21+
end
22+
end

lib/key_store.ex

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
defmodule ExFirebaseAuth.KeyStore do
2+
@moduledoc false
3+
4+
use GenServer, restart: :transient
5+
6+
require Logger
7+
8+
@endpoint_url "https://www.googleapis.com/robot/v1/metadata/x509/[email protected]"
9+
10+
def start_link(_) do
11+
GenServer.start_link(__MODULE__, %{}, name: ExFirebaseAuth.KeyStore)
12+
end
13+
14+
def init(_) do
15+
case fetch_certificates() do
16+
# when we could not fetch certs initially the application cannot run because all Auth will fail
17+
:error -> {:stop, "Initial certificate fetch failed"}
18+
{:ok, data} -> {:ok, data}
19+
end
20+
end
21+
22+
# When the refresh `info` is sent, we want to fetch the certificates
23+
def handle_info(:refresh, state) do
24+
case fetch_certificates() do
25+
# keep trying with a lower interval, until then keep the old state
26+
:error ->
27+
Logger.warn("Fetching firebase auth certificates failed, using old state and retrying...")
28+
schedule_refresh(10)
29+
{:noreply, state}
30+
31+
# if everything went okay, refresh at the regular interval and store the returned keys in state
32+
{:ok, jsondata} ->
33+
Logger.debug("Fetched new firebase auth certificates")
34+
schedule_refresh()
35+
{:noreply, jsondata}
36+
end
37+
end
38+
39+
# Use a {:get, id} call to get the JWK struct of the certificate with the given key id. Returns nil if not found
40+
# Usage: `GenServer.call(ExFirebaseAuth.KeyStore, {:get, keyid})`
41+
def handle_call({:get, key_id}, _from, state) do
42+
case Map.get(state, key_id) do
43+
nil -> {:reply, nil, state}
44+
key -> {:reply, JOSE.JWK.from_pem(key), state}
45+
end
46+
end
47+
48+
defp schedule_refresh(after_s \\ 3600) do
49+
Process.send_after(self(), :refresh, after_s * 1000)
50+
end
51+
52+
# Fetch certificates from google's endpoint
53+
defp fetch_certificates do
54+
with {:ok, %Finch.Response{body: body}} <-
55+
Finch.build(:get, @endpoint_url) |> Finch.request(ExFirebaseAuthFinch),
56+
{:ok, jsondata} <- Jason.decode(body) do
57+
{:ok, jsondata}
58+
else
59+
_ ->
60+
:error
61+
end
62+
end
63+
end

lib/token.ex

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
defmodule ExFirebaseAuth.Token do
2+
defp get_public_key(keyid) do
3+
GenServer.call(ExFirebaseAuth.KeyStore, {:get, keyid})
4+
end
5+
6+
@spec verify_token(binary) ::
7+
{:error, binary} | {:ok, binary(), JOSE.JWT.t()}
8+
@doc """
9+
Verifies a token agains google's public keys. Returns {:ok, sub, data} if successful. {:error, _} otherwise.
10+
"""
11+
def verify_token(token_string) do
12+
issuer = Application.fetch_env!(:ex_firebase_auth, :issuer)
13+
14+
with {:jwtheader, %{fields: %{"kid" => kid}}} <-
15+
{:jwtheader, JOSE.JWT.peek_protected(token_string)},
16+
# read key from store
17+
{:key, %JOSE.JWK{} = key} <- {:key, get_public_key(kid)},
18+
# check if verify returns true and issuer matches
19+
{:verify, {true, %{fields: %{"iss" => ^issuer, "sub" => sub}} = data, _}} <-
20+
{:verify, JOSE.JWT.verify(key, token_string)} do
21+
{:ok, sub, data}
22+
else
23+
{:jwtheader, _} ->
24+
{:error, "Invalid JWT header, `kid` missing"}
25+
26+
{:key, _} ->
27+
{:error, "Public key retrieved from google could not be parsed"}
28+
29+
{:verify, _} ->
30+
{:error, "None of public keys matched auth token's key ids"}
31+
end
32+
end
33+
end

mix.exs

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
defmodule ExFirebaseAuth.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :ex_firebase_auth,
7+
version: "0.1.0",
8+
elixir: "~> 1.11",
9+
start_permanent: Mix.env() == :prod,
10+
deps: deps()
11+
]
12+
end
13+
14+
# Run "mix help compile.app" to learn about applications.
15+
def application do
16+
[
17+
mod: {ExFirebaseAuth, []},
18+
extra_applications: [:logger],
19+
registered: [ExFirebaseAuth.KeyStore]
20+
]
21+
end
22+
23+
# Run "mix help deps" to learn about dependencies.
24+
defp deps do
25+
[
26+
{:jose, "~> 1.10"},
27+
{:finch, "~> 0.3.1"},
28+
{:jason, "~> 1.2.2"}
29+
]
30+
end
31+
end

mix.lock

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
%{
2+
"castore": {:hex, :castore, "0.1.8", "1b61eaba71bb755b756ac42d4741f4122f8beddb92456a84126d6177ec0af1fc", [:mix], [], "hexpm", "23ab8305baadb057bc689adc0088309f808cb2247dc9a48b87849bb1d242bb6c"},
3+
"finch": {:hex, :finch, "0.3.2", "993dddb23cd9b50ad395ed994b317c7ce2c095e6017ffd5a5b65abdb5b7e5d1c", [:mix], [{:castore, "~> 0.1.5", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.2.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "85be155ad3f6a1d806610f2b83e054d3201dc2fd1c4e97d47f40687ad877193a"},
4+
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
5+
"jose": {:hex, :jose, "1.11.1", "59da64010c69aad6cde2f5b9248b896b84472e99bd18f246085b7b9fe435dcdb", [:mix, :rebar3], [], "hexpm", "078f6c9fb3cd2f4cfafc972c814261a7d1e8d2b3685c0a76eb87e158efff1ac5"},
6+
"mint": {:hex, :mint, "1.2.0", "65e9d75c60c456a5fb1b800febb88f061f56157d103d755b99fcaeaeb3e956f3", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "19cbb3a5be91b7df4a35377ba94b26199481a541add055cf5d1d4299b55125ab"},
7+
"nimble_options": {:hex, :nimble_options, "0.2.1", "7eac99688c2544d4cc3ace36ee8f2bf4d738c14d031bd1e1193aab096309d488", [:mix], [], "hexpm", "ca48293609306791ce2634818d849b7defe09330adb7e4e1118a0bc59bed1cf4"},
8+
"nimble_pool": {:hex, :nimble_pool, "0.1.0", "ffa9d5be27eee2b00b0c634eb649aa27f97b39186fec3c493716c2a33e784ec6", [:mix], [], "hexpm", "343a1eaa620ddcf3430a83f39f2af499fe2370390d4f785cd475b4df5acaf3f9"},
9+
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
10+
}

test/ex_firebase_auth_test.exs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
defmodule ExFirebaseAuthTest do
2+
use ExUnit.Case
3+
end

test/test_helper.exs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ExUnit.start()

0 commit comments

Comments
 (0)