Firebase token authentication strategy for AshAuthentication.
- AshAuthentication ~> 4.0
The package can be installed by adding ash_authentication_firebase to your list of dependencies in mix.exs:
def deps do
[
{:ash_authentication_firebase, "~> 1.0"}
]
endPlease consult the official AshAuthentication docs 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.
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
endSet 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.
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
endTo 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.
Using an anonymous function:
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
endUsing a module:
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
endThis library performs the token-verification checks documented by Firebase:
- Header:
algisRS256andkidmatches one of Google's currently published public keys. - Signature: the token is signed by the matching key.
- Claims:
issishttps://securetoken.google.com/<project_id>,audis<project_id>,subis a non-empty string,expis in the future,iatandauth_timeare in the past (each within:clock_skew_leeway_seconds, default 60s, valid range0..300). - Email verification (when
require_email_verified?istrue, the default): tokens whoseemailclaim is present and non-empty must also haveemail_verifiedset to the literal booleantrue. Tokens without anemailclaim (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)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.
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} |
:fetchedfires after a successful refresh of Google's public keys.expires_inis the milliseconds until the next scheduled refresh derived from the response'sCache-Control: max-age.retry_attemptreports how many failed attempts preceded this success (0on the happy path).:fetch_failedfires whenever a key fetch fails.delayis the milliseconds until the next retry;reasonis the underlying error (aMint.TransportError, an HTTP status string,:timeout,:no_valid_keys,:invalid_key_response, etc.).:token_rejectedfires when token verification fails at the strategy boundary.reasonis one of the values listed inAshAuthentication.Firebase.Errors.InvalidToken'st:reason/0type (e.g.:invalid_signature,:expired,:invalid_audience);strategyis the strategy name configured in the DSL.:missing_secretfires when sign-in fails because a required secret (currently:project_id) is unset or resolves to a blank value.pathis the DSL path of the missing secret;strategyis the strategy name. Distinguishes operator misconfiguration from end-user bad-token traffic, which both surface to the client asInvalidToken.
Inspired by ExFirebaseAuth.