# AshAuthentication.Firebase
[](https://github.com/r8/ash_authentication_firebase/actions/workflows/elixir.yml)
[](https://hex.pm/packages/ash_authentication_firebase)
[](https://hex.pm/packages/ash_authentication_firebase)
Firebase token authentication strategy for [AshAuthentication](https://github.com/team-alembic/ash_authentication).
## Requirements
- [AshAuthentication](https://github.com/team-alembic/ash_authentication) ~> 4.0
## Installation
The package can be installed by adding `ash_authentication_firebase` to your list of dependencies in mix.exs:
```elixir
def deps do
[
{:ash_authentication_firebase, "~> 1.0"}
]
end
```
## Usage
Please consult the official [AshAuthentication docs](https://hexdocs.pm/ash_authentication/get-started.html) for how to scaffold a resource. Below is a complete minimal example showing the parts this strategy requires.
Add `AshAuthentication.Strategy.Firebase` to your resource `extensions`, declare an `identity` keyed on the Firebase user id, and define a `create` action with `upsert?: true` / `upsert_identity:` so repeat sign-ins update the existing user. The strategy validates this shape at compile time and raises a `Spark.Error.DslError` if anything is missing.
```elixir
defmodule MyApp.Accounts.User do
use Ash.Resource,
domain: MyApp.Accounts,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication, AshAuthentication.Strategy.Firebase]
attributes do
uuid_primary_key :id
attribute :uid, :string, allow_nil?: false, public?: true
attribute :email, :string, public?: true
end
identities do
identity :unique_uid, [:uid]
end
actions do
defaults [:read]
create :register_with_firebase do
argument :user_info, :map, allow_nil?: false
upsert? true
upsert_identity :unique_uid
change fn changeset, _ ->
info = Ash.Changeset.get_argument(changeset, :user_info)
changeset
|> Ash.Changeset.change_attribute(:uid, info["uid"])
|> Ash.Changeset.change_attribute(:email, info["email"])
end
end
end
authentication do
strategies do
# Multiple firebase strategies with different project_ids are supported.
firebase :firebase do
project_id "project-123abc"
token_input :firebase_token
end
end
end
end
```
### Sign-in only (no auto-registration)
Set `registration_enabled?(false)` and replace the `create` action with a `read` action — useful when users must be provisioned out-of-band before they can sign in.
```elixir
actions do
defaults [:read]
read :sign_in_with_firebase do
argument :user_info, :map, allow_nil?: false
get? true
prepare fn query, _ ->
uid = Ash.Query.get_argument(query, :user_info)["uid"]
Ash.Query.filter(query, uid == ^uid)
end
end
end
authentication do
strategies do
firebase :firebase do
project_id "project-123abc"
token_input :firebase_token
registration_enabled? false
end
end
end
```
## Secrets and Runtime Configuration
To avoid hardcoding your Firebase project id in your source code, you can use the `AshAuthentication.Secret` behaviour. This allows you to provide the project id through runtime configuration using either an anonymous function or a module.
### Examples:
Using an anonymous function:
```elixir
authentication do
strategies do
firebase :firebase do
project_id fn _path, _resource ->
Application.fetch_env(:my_app, :firebase_project_id)
end
token_input :firebase_token
end
end
end
```
Using a module:
```elixir
defmodule MyApp.Secrets do
use AshAuthentication.Secret
def secret_for([:authentication, :strategies, :firebase, :project_id], MyApp.Accounts.User, _opts, _context) do
Application.fetch_env(:my_app, :firebase_project_id)
end
end
# And in your resource:
authentication do
strategies do
firebase :firebase do
project_id MyApp.Secrets
token_input :firebase_token
end
end
end
```
## Security model
This library performs the [token-verification checks documented by Firebase](https://firebase.google.com/docs/auth/admin/verify-id-tokens):
- **Header**: `alg` is `RS256` and `kid` matches one of Google's currently published public keys.
- **Signature**: the token is signed by the matching key.
- **Claims**: `iss` is `https://securetoken.google.com/<project_id>`, `aud` is `<project_id>`, `sub` is a non-empty string, `exp` is in the future, `iat` and `auth_time` are in the past (each within `:clock_skew_leeway_seconds`, default 60s, valid range `0..300`).
- **Email verification** (when `require_email_verified?` is `true`, the default): tokens whose `email` claim is present and non-empty must also have `email_verified` set to the literal boolean `true`. Tokens without an `email` claim (phone-auth, anonymous) are unaffected.
What this library does **not** verify:
- **Revocation / disabled users.** Firebase ID tokens do not reflect server-side state changes until they expire (up to one hour). If you need immediate logout on password reset, account disablement, or admin ban, layer that check inside your Ash sign-in / register action — e.g. consult a "revoked_at" attribute on the user or call Firebase Admin's [`verifyIdToken(..., checkRevoked = true)`](https://firebase.google.com/docs/auth/admin/manage-sessions#detect_id_token_revocation) from a custom change.
- **Custom claim policies.** All custom claims pass through untouched; enforcing role / tenant constraints based on them is your resource's responsibility.
## Telemetry
The library emits the following `:telemetry` events. Attach handlers to pipe them into your observability stack of choice (Prometheus, StatsD, etc.).
| Event | Measurements | Metadata |
| --- | --- | --- |
| `[:ash_authentication_firebase, :key_store, :fetched]` | `%{retry_attempt, keys_count, expires_in}` | `%{}` |
| `[:ash_authentication_firebase, :key_store, :fetch_failed]` | `%{retry_attempt, delay}` | `%{reason}` |
| `[:ash_authentication_firebase, :strategy, :token_rejected]` | `%{count: 1}` | `%{reason, strategy}` |
| `[:ash_authentication_firebase, :strategy, :missing_secret]` | `%{count: 1}` | `%{strategy, path}` |
- `:fetched` fires after a successful refresh of Google's public keys. `expires_in` is the milliseconds until the next scheduled refresh derived from the response's `Cache-Control: max-age`. `retry_attempt` reports how many failed attempts preceded this success (`0` on the happy path).
- `:fetch_failed` fires whenever a key fetch fails. `delay` is the milliseconds until the next retry; `reason` is the underlying error (a `Mint.TransportError`, an HTTP status string, `:timeout`, `:no_valid_keys`, `:invalid_key_response`, etc.).
- `:token_rejected` fires when token verification fails at the strategy boundary. `reason` is one of the values listed in `AshAuthentication.Firebase.Errors.InvalidToken`'s `t:reason/0` type (e.g. `:invalid_signature`, `:expired`, `:invalid_audience`); `strategy` is the strategy name configured in the DSL.
- `:missing_secret` fires when sign-in fails because a required secret (currently `:project_id`) is unset or resolves to a blank value. `path` is the DSL path of the missing secret; `strategy` is the strategy name. Distinguishes operator misconfiguration from end-user bad-token traffic, which both surface to the client as `InvalidToken`.
## Acknowledgements
Inspired by [ExFirebaseAuth](https://github.com/Nickforall/ExFirebaseAuth).