README.md

# 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.2.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_attrs: 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_attrs: 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"
```

### 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