Fireauth

Firebase Auth helpers for Elixir apps:

Install

Add to your mix.exs

{:fireauth, "~> 0.4.0"},

Two Ways To Use This Library

1) ID Tokens (Bearer Headers, No Admin Credentials)

This is the simplest integration if your client is JavaScript and can send:

Authorization: Bearer <idToken>

Fireauth will verify that ID token and attach claims/user info to conn.assigns. This does not require any Firebase Admin service account.

Minimal Plug Example

defmodule MyApp.Router do
  use Plug.Router
  import Plug.Conn

  plug :match

  # Verifies `Authorization: Bearer <idToken>` and assigns `conn.assigns.fireauth`.
  plug Fireauth.Plug,
    serve_hosted_auth?: false,
    # Optional if you set `config :fireauth, firebase_project_id: "..."` (or `FIREBASE_PROJECT_ID`).
    project_id: "your-project-id",
    on_invalid_token: :unauthorized

  plug :dispatch

  get "/protected" do
    case conn.assigns[:fireauth] do
      %{claims: claims, user: user} ->
        json(conn, 200, %{
          "uid" => user.firebase_uid,
          "email" => user.email,
          "iss" => claims.iss,
          "aud" => claims.aud
        })

      _ ->
        send_resp(conn, 401, "unauthorized")
    end
  end

  match _ do
    send_resp(conn, 404, "not_found")
  end

  defp json(conn, status, data) do
    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(data))
  end
end

2) Session Cookies (Phoenix/LiveView, Requires Admin Credentials To Mint)

If you want a traditional Phoenix/LiveView setup using an httpOnly cookie:

  1. Client signs in with Firebase JS SDK and obtains an idToken
  2. Client POSTs that idToken to your backend
  3. Backend exchanges it for a Firebase session cookie and sets it as httpOnly

That exchange requires admin credentials (typically a Firebase Admin service account). You do not need the Admin SDK library, but you do need the service account credentials.

Minimal Plug Example

defmodule MyApp.Router do
  use Plug.Router
  import Plug.Conn

  plug :match

  # 1) Mount endpoints for:
  #    GET  /auth/csrf
  #    POST /auth/session  (exchange idToken -> httpOnly session cookie)
  #    POST /auth/logout
  forward "/auth",
    to: Fireauth.Plug.SessionRouter,
    init_opts: [
      # Optional if you set `config :fireauth, firebase_project_id: "..."` (or `FIREBASE_PROJECT_ID`).
      project_id: "your-project-id",
      # For local dev only; in prod you want true.
      cookie_secure: false,
      # Optional. Default is 5 days. Must be between 300 and 1_209_600 seconds.
      valid_duration_s: 60 * 60 * 24 * 14
    ]

  # 2) Verify the httpOnly cookie on every request and assign `conn.assigns.fireauth`.
  plug Fireauth.Plug.SessionCookie,
    # Optional if you set `config :fireauth, firebase_project_id: "..."` (or `FIREBASE_PROJECT_ID`).
    project_id: "your-project-id",
    on_invalid_cookie: :unauthorized

  plug :dispatch

  get "/protected" do
    case conn.assigns[:fireauth] do
      %{claims: claims, user: user} ->
        json(conn, 200, %{
          "uid" => user.firebase_uid,
          "email" => user.email,
          "iss" => claims.iss,
          "aud" => claims.aud
        })

      _ ->
        send_resp(conn, 401, "unauthorized")
    end
  end

  match _ do
    send_resp(conn, 404, "not_found")
  end

  defp json(conn, status, data) do
    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(data))
  end
end

Configuration

Set your Firebase project id:

config :fireauth, firebase_project_id: "your-project-id"

Or via env var: FIREBASE_PROJECT_ID.

Session Cookies (Admin Service Account)

To mint session cookies, configure a Firebase Admin service account (JSON) either in config or via env var.

Config:

config :fireauth, firebase_admin_service_account: %{
  "client_email" => "firebase-adminsdk-...@your-project.iam.gserviceaccount.com",
  "private_key" => "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
  "project_id" => "your-project-id"
}

Env var (JSON or base64-encoded JSON):

Usage

Token Verification & Identity Helpers

{:ok, claims} = Fireauth.verify_id_token(id_token)
user = Fireauth.User.from_claims(claims)

# Check for specific identities (works with both claims and user structs)
if Fireauth.has_identity?(user, "google.com") do
  google_uid = Fireauth.identity(user, "google.com")
end

Create Session Cookie

{:ok, session_cookie} = Fireauth.create_session_cookie(id_token, valid_duration_s: 60 * 60 * 24 * 5)

This makes a network call to Google (Identity Toolkit) to exchange the ID token for a session cookie, and requires service account credentials.

Hosted Auth Files (Optional)

Fireauth.Plug can also proxy or serve Firebase hosted auth helper files at /__/auth/* and /__/firebase/init.json. Enable this if you want redirect-mode auth to work on your own domain in modern browsers.

plug Fireauth.Plug,
  # Enable hosted auth file handling.
  serve_hosted_auth?: true,
  # :proxy (default) fetches from firebaseapp.com. :static serves local copies.
  hosted_auth_mode: :proxy,
  # Optional if you set `config :fireauth, firebase_project_id: "..."` (or `FIREBASE_PROJECT_ID`).
  project_id: "your-project-id"

Redirect-Mode Client Helper (Optional)

Fireauth ships a JavaScript helper via Fireauth.Snippets.client/1 to make this flow easier:

  1. Redirect the user to a /start/ page with POST: Starts firebase auth flow, redirects to oauth screen, etc
  2. Firebase redirects back to the /start/ with GET: Show loading indicator, exchange idToken for session cookie, redirect to main page

It exposes:

Optional UI integration:

Server Setup

  1. Ensure hosted auth files are served at the endpoint level (so /__/auth/handler is not a 404):
# In your Endpoint (Phoenix) or top-level Plug stack (Plug.Router),
# before your router.
plug Fireauth.Plug, hosted_auth_mode: :proxy
  1. Mount the session endpoints (cookie minting) and verify cookies on requests:
forward "/auth/firebase",
  to: Fireauth.Plug.SessionRouter,
  init_opts: [cookie_secure: false]

plug Fireauth.Plug.SessionCookie

Snippet Setup

In any HEEx template (requires phoenix_html), embed the snippet:

{Fireauth.Snippets.client(
  return_to: @return_to,
  session_base: "/auth/firebase",
  debug: true
)}

Start Flow

<script>
  function buildProvider(providerId) {
    const authNs = window.firebase.auth;

    if (
      providerId.indexOf("github") !== -1 &&
      typeof authNs.GithubAuthProvider === "function"
    ) {
      return new authNs.GithubAuthProvider();
    }

    if (
      providerId.indexOf("google") !== -1 &&
      typeof authNs.GoogleAuthProvider === "function"
    ) {
      return new authNs.GoogleAuthProvider();
    }

    return null;
  }

  fireauth
    .start({ provider: "github.com" }, function (providerId, ctx) {
      const auth = window.firebase.auth.getAuth();
      const authNs = window.firebase.auth;
      const signInWithRedirect = authNs.signInWithRedirect;

      const provider = buildProvider(providerId);
      if (!provider) {
        throw new Error("Unsupported provider: " + String(providerId || ""));
      }

      return signInWithRedirect(auth, provider);
    })
    .error(function (s) {
      console.warn("start error", s.code, s.message);
    })
    .onStateChange(function (s) {
      console.debug("start state", s.stage);
    });
</script>

Verify Flow

<script>
  fireauth
    .verify(
      { requireVerified: true, getAuth: window.firebase.auth.getAuth },
      function (s) {
        if (!s) return;
        if (s.type === "error")
          return showError(s.message || statusEl.textContent);
        if (s.loading) return showLoading(s.message || statusEl.textContent);
      },
    )
    .success(function () {
      showLoading("Login successful. Redirecting...");
    })
    .error(function (s) {
      showError((s && s.message) || statusEl.textContent);
    });
</script>

Hosted Auth Modes

To support redirect-mode auth in modern browsers (avoiding third-party cookie issues), you must serve Firebase's helper files from your own domain.

  1. :proxy (Default): Transparently proxies requests to https://<project>.firebaseapp.com. This is the most robust method. Responses are cached in-memory.
  2. :static: Serves local copies of the helper files embedded in the fireauth library. Use this if your environment cannot make outbound requests to Firebase at runtime.

Caching

This library caches /__/auth/* calls in addition to the Google public keys used to verify ID tokens and session cookies.

License

MIT