Skip to content

r8/ash_authentication_firebase

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

87 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AshAuthentication.Firebase

Elixir CI Hex.pm Hex.pm

Firebase token authentication strategy for AshAuthentication.

Requirements

Installation

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"}
  ]
end

Usage

Please 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
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.

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:

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:

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:

  • 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) 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.

About

Firebase token authentication strategy for AshAuthentication.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages