# Fireauth
Firebase Auth helpers for Elixir apps:
- Verify Firebase ID tokens (RS256) using Google's SecureToken x509 certs.
- (Optional) Mint Firebase session cookies (server-side, requires admin credentials via a service account).
- Plug helpers for both approaches.
## 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
```elixir
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
```elixir
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:
```elixir
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:
```elixir
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):
- `FIREBASE_ADMIN_SERVICE_ACCOUNT='{"type":"service_account",...}'`
- `FIREBASE_ADMIN_SERVICE_ACCOUNT='<base64-json>'`
## Usage
### Token Verification & Identity Helpers
```elixir
{: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
```elixir
{: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.
```elixir
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:
- `window.fireauth.start(opts, callback)`:
- executes your callback to start Firebase redirect
- supports `opts.ready` — a function polled until truthy before invoking the callback (useful when the Firebase SDK loads asynchronously via a deferred script)
- supports `opts.readyTimeout` — max ms to wait (default 5000)
- `window.fireauth.verify(opts, callback)`:
- resolves current user from `opts.getAuth()`
- calls `currentUser.getIdToken()`
- exchanges `idToken` for session cookie
- redirects to `return_to`
- supports chaining: `.success(...).error(...).onStateChange(...)`
Optional UI integration:
- `window.fireauth.onStateChange/1`, `window.fireauth.onError/1`, `window.fireauth.onSuccess/1`
#### Server Setup
1. Ensure hosted auth files are served at the endpoint level (so `/__/auth/handler` is not a 404):
```elixir
# In your Endpoint (Phoenix) or top-level Plug stack (Plug.Router),
# before your router.
plug Fireauth.Plug, hosted_auth_mode: :proxy
```
2. Mount the session endpoints (cookie minting) and verify cookies on requests:
```elixir
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:
```elixir
{Fireauth.Snippets.client(
return_to: @return_to,
session_base: "/auth/firebase",
debug: true
)}
```
#### Start Flow
```html
<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",
// Wait for the Firebase SDK to be available before starting.
// Useful when the app bundle is loaded with <script defer>.
ready: function () {
return !!window.myFirebaseAuth;
},
// Optional: max ms to wait (default 5000)
// readyTimeout: 3000,
},
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
```html
<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