XUtils

guardian

An authentication framework for use with Elixir applications. ([Docs](https://hexdocs.pm/guardian/Guardian.html)).


Documentation

API documentation is available at https://hexdocs.pm/guardian

Basics

# encode a token for a resource
{:ok, token, claims} = MyApp.Guardian.encode_and_sign(resource)

# decode and verify a token
{:ok, claims} = MyApp.Guardian.decode_and_verify(token)

# revoke a token (use GuardianDb or something similar if you need revoke to actually track a token)
{:ok, claims} = MyApp.Guardian.revoke(token)

# Refresh a token before it expires
{:ok, _old_stuff, {new_token, new_claims}} = MyApp.Guardian.refresh(token)

# Exchange a token of type "refresh" for a new token of type "access"
{:ok, _old_stuff, {new_token, new_claims}} = MyApp.Guardian.exchange(token, "refresh", "access")

# Lookup a resource directly from a token
{:ok, resource, claims} = MyApp.Guardian.resource_from_token(token)

With Plug:

# The token/resource/claims will be stored on the connection.
# The token will also be stored in the session (if fetched)
conn = MyApp.Guardian.Plug.sign_in(conn, resource)

# Optionally with claims and options
conn = MyApp.Guardian.Plug.sign_in(conn, resource, %{some: "claim"}, ttl: {1, :minute})

# remove from session (if fetched) and revoke the token
# can also clear the remember me token, if the option :clear_remember_me is set
conn = MyApp.Guardian.Plug.sign_out(conn)

# Set a "refresh" token directly on a cookie.
# Can be used in conjunction with `Guardian.Plug.VerifyCookie` and `Guardian.Plug.SlidingCookie`
conn = MyApp.Guardian.Plug.remember_me(conn, resource)

# Fetch the information from the current connection
token = MyApp.Guardian.Plug.current_token(conn)
claims = MyApp.Guardian.Plug.current_claims(conn)
resource = MyApp.Guardian.Plug.current_resource(conn)

Creating with custom claims and options:

# Add custom claims to a token
{:ok, token, claims} = MyApp.Guardian.encode_and_sign(resource, %{some: "claim"})

# Create a specific token type (i.e. "access"/"refresh" etc)
{:ok, token, claims} = MyApp.Guardian.encode_and_sign(resource, %{}, token_type: "refresh")

# Customize the time to live (ttl) of the token
{:ok, token, claims} = MyApp.Guardian.encode_and_sign(resource, %{}, ttl: {1, :minute})

# Customize the secret
{:ok, token, claims} = MyApp.Guardian.encode_and_sign(resource, %{}, secret: "custom")
{:ok, token, claims} = MyApp.Guardian.encode_and_sign(resource, %{}, secret: {SomeMod, :some_func, ["some", "args"]})

# Require an "auth_time" claim to be added.
{:ok, token, claims} = MyApp.Guardian.encode_and_sign(resource, %{}, auth_time: true)

Decoding tokens:

# Check some literal claims. (i.e. this is an access token)
{:ok, claims} = MyApp.Guardian.decode_and_verify(token, %{"typ" => "access"})

# Use a custom secret
{:ok, claims} = MyApp.Guardian.decode_and_verify(token, %{}, secret: "custom")
{:ok, claims} = MyApp.Guardian.decode_and_verify(token, %{}, secret: {SomeMod, :some_func, ["some", "args"]})

# Specify a maximum age (since end user authentication time). If the token has an
# `auth_time` claim and it is older than the `max_age` allows, the token will be invalid.
{:ok, claims} = MyApp.Guardian.decode_and_verify(token, %{}, max_age: {2, :hours})

If you need dynamic verification for JWT tokens, please see the documentation for Guardian.Token.Jwt and Guardian.Token.Jwt.SecretFetcher.

Configuration

The following configuration is available to all implementation modules.

  • token_module - The module that implements the functions for dealing with tokens. Default Guardian.Token.Jwt.

Guardian can handle tokens of any type that implements the Guardian.Token behaviour. Each token module will have its own configuration requirements. Please see below for the JWT configuration.

All configuration values may be provided in two ways.

  1. In your config files
  2. As a Keyword list to your call to use Guardian in your implementation module.

Any options given to use Guardian have precedence over config values found in the config files.

Some configuration may be required by your token_module.

JWT (Configuration)

The default token type of Guardian is JWT. It accepts many options but you really only need to specify the issuer and secret_key.

Required configuration (JWT)

  • issuer - The issuer of the token. Your application name/id
  • secret_key - The secret key to use for the implementation module. This may be any resolvable value for Guardian.Config.

Optional configuration (JWT)

  • token_verify_module - default Guardian.Token.Jwt.Verify. The module that verifies the claims
  • allowed_algos - The allowed algos to use for encoding and decoding. See JOSE for available. Default ["HS512"]
  • ttl - The default time to live for all tokens. See the type in Guardian.ttl
  • token_ttl a map of token_type to ttl. Set specific ttls for specific types of tokens
  • allowed_drift The drift that is allowed when decoding/verifying a token in milliseconds
  • verify_issuer Default false
  • secret_fetcher A module used to fetch the secret. Default: Guardian.Token.Jwt.SecretFetcher
  • auth_time Include an auth_time claim to denote the end user authentication time. Default false.
  • max_age Specify the maximum time (since the end user authentication) the token will be valid. Format is the same as ttl. Implies auth_time unless auth_time is set explicitly to false.

See the OpenID Connect Core specification for more details about auth_time and max_age behaviour.

Secrets (JWT)

Secrets can be simple strings or more complicated JOSE secret schemes.

The simplest way to use the JWT module is to provide a simple String. (mix guardian.gen.secret works great)

Alternatively you can use a module and function by adding secret_key: {MyModule, :function_name, [:some, :args]}.

More advanced secret information can be found below.

Using options in calls

Almost all of the functions provided by Guardian utilize options as the last argument. These options are passed from the initiating call through to the token_module and also your callbacks. See the documentation for your token_module (Guardian.Token.Jwt by default) for more information.

Hooks

Each implementation module (modules that use Guardian) implement callbacks for the Guardian behaviour. By default, these are just pass-through but you can implement your own version to tweak the behaviour of your tokens.

The callbacks are:

  • after_encode_and_sign
  • after_sign_in
  • before_sign_out
  • build_claims - Use this to tweak the claims that you include in your token
  • default_token_type - default is "access"
  • on_exchange
  • on_revoke
  • on_refresh
  • on_verify
  • verify_claims - You can add custom validations for your tokens in this callback

Plugs

Guardian provides various plugs to help work with web requests in Elixir. Guardian’s plugs are optional and will not be compiled if you’re not using Plug in your application.

All plugs need to be in a pipeline. A pipeline is just a way to get the implementation module and error handler into the connection for use downstream. More information can be found in the Pipelines section.

Plugs out of the box

Guardian.Plug.VerifyHeader

Look for a token in the header and verify it

Guardian.Plug.VerifySession

Look for a token in the session and verify it

Guardian.Plug.VerifyCookie

NOTE: this plug is deprecated. Please use :refresh_from_cookie option in Guardian.Plug.VerifyHeader or Guardian.Plug.VerifySession

Look for a token in cookies and exchange it for an access token

Guardian.Plug.SlidingCookie

Replace the token in cookies with a new one when a configured minimum TTL is remaining.

Guardian.Plug.EnsureAuthenticated

Make sure that a token was found and is valid

Guardian.Plug.EnsureNotAuthenticated

Make sure no one is logged in

Guardian.Plug.LoadResource

If a token was found, load the resource for it

See the documentation for each Plug for more information.

Pipelines

A pipeline is a way to collect together the various plugs for a particular authentication scheme.

Apart from keeping an authentication flow together, pipelines provide downstream information for error handling and which implementation module to use. You can provide this separately but we recommend creating a pipeline plug.

Create a custom pipeline

defmodule MyApp.AuthAccessPipeline do
  use Guardian.Plug.Pipeline, otp_app: :my_app

  plug Guardian.Plug.VerifySession, claims: %{"typ" => "access"}
  plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"}
  plug Guardian.Plug.EnsureAuthenticated
  plug Guardian.Plug.LoadResource, allow_blank: true
end

By default, the LoadResource plug will return an error if no resource can be found. You can override this behaviour using the allow_blank: true option.

Add your implementation module and error handler to your configuration:

config :my_app, MyApp.AuthAccessPipeline,
  module: MyApp.Guardian,
  error_handler: MyApp.AuthErrorHandler

By using a pipeline, apart from keeping your auth logic together, you’re instructing downstream plugs to use a particular implementation module and error handler.

If you wanted to do that manually:

plug Guardian.Plug.Pipeline, module: MyApp.Guardian,
                             error_handler: MyApp.AuthErrorHandler

plug Guardian.Plug.VerifySession

Plug Error Handlers

The error handler is a module that implements an auth_error function:

defmodule MyApp.AuthErrorHandler do
  import Plug.Conn

  @behaviour Guardian.Plug.ErrorHandler

  @impl Guardian.Plug.ErrorHandler
  def auth_error(conn, {type, _reason}, _opts) do
    body = Jason.encode!(%{message: to_string(type)})

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(401, body)
  end
end

Phoenix

Guardian and Phoenix are perfect together, but to get the most out of the integration be sure to include the guardian_phoenix library.

See the Guardian Phoenix documentation for more information.

Permissions

Permissions can be encoded into your token as an optional add-in.

Encoding permissions into a token is useful in some areas of authorization. The permissions provided by Guardian.Permissions have one level of nesting.

For example:

  • users -> profile_read
  • users -> profile_write
  • users -> followers_read
  • users -> followers_write
  • admin -> all_users_read
  • admin -> all_users_write

Once a permission is granted it is valid for as long as the token is valid. Since the permission is valid for the life of a token it is not suitable to encode highly dynamic information into a token. These permissions are similar in intent to OAuth scopes. Very useful as a broad grant to an area of code for 3rd party services / other microservices. If you have a requirement to look up permissions from your database for a particular user on each request, these are not the permissions you’re looking for.

Please see Guardian.Permissions for more information.

Tracking Tokens

When using tokens, depending on the type of token you use, nothing may happen by default when you revoke a token.

For example, JWT tokens by default are not tracked by the application. The fact that they are signed with the correct secret and are not expired is usually how validation of if a token is active or not. Depending on your use-case this may not be enough for your application needs. If you need to track and revoke individual tokens, you may need to use something like GuardianDb.

This will record each token issued in your database, confirm it is still valid on each access and then finally when you revoke (called on sign_out or manually) invalidate the token.

For more in-depth documentation please see the GuardianDb README.

Best testing practices

How to add the token to a request (the Phoenix way)

Assuming you are using the default authentication scheme Bearer for the Authorization header:

defmodule HelloWeb.AuthControllerTest do
  use HelloWeb.ConnCase
  import HelloWeb.Guardian

 test "GET /auth/me", %{conn: conn} do
    user = insert(:user) # See https://github.com/thoughtbot/ex_machina

    {:ok, token, _} = encode_and_sign(user, %{}, token_type: :access)

    conn = conn
    |> put_req_header("authorization", "Bearer " <> token)
    |> get(auth_path(conn, :me))

    # Assert things here
  end

end
  • GuardianDb - Token tracking in the database
  • GuardianPhoenix - Phoenix integration
  • sans_password - A simple, passwordless authentication system based on Guardian.
  • protego - Flexible authentication solution for Elixir/Phoenix with Guardian.

More advanced secrets

By specifying a binary, the default behavior is to treat the key as an "oct" key type (short for octet sequence). This key type may be used with the "HS256", "HS384", and "HS512" signature algorithms.

Alternatively, a configuration value that resolves to:

  • Map
  • Function
  • %JOSE.JWK{} Struct

May be specified for other key types. A full list of example key types is available here.

See the key generation docs from Jose for how to generate your own keys.

To get off the ground quickly, set your secret_key in your Guardian config with the output of either:

$ mix guardian.gen.secret`

or

iex> JOSE.JWS.generate_key(%{"alg" => "HS512"}) |> JOSE.JWK.to_map |> elem(1) |> Map.take(["k", "kty"])

After running $ mix deps.get because JOSE is one of Guardian’s dependencies:

## Map ##

config :my_app, MyApp.Guardian,
  allowed_algos: ["ES512"],
  secret_key: %{
    "crv" => "P-521",
    "d" => "axDuTtGavPjnhlfnYAwkHa4qyfz2fdseppXEzmKpQyY0xd3bGpYLEF4ognDpRJm5IRaM31Id2NfEtDFw4iTbDSE",
    "kty" => "EC",
    "x" => "AL0H8OvP5NuboUoj8Pb3zpBcDyEJN907wMxrCy7H2062i3IRPF5NQ546jIJU3uQX5KN2QB_Cq6R_SUqyVZSNpIfC",
    "y" => "ALdxLuo6oKLoQ-xLSkShv_TA0di97I9V92sg1MKFava5hKGST1EKiVQnZMrN3HO8LtLT78SNTgwJSQHAXIUaA-lV"
  }

## Tuple ##
# If, for example, you have your secret key stored externally (in this example, we're using Redix).

# defined elsewhere
defmodule MySecretKey do
  def fetch do
    # Bad practice for example purposes only.
    # An already established connection should be used and possibly cache the value locally.
    {:ok, conn} = Redix.start_link
    rsa_jwk = conn
      |> Redix.command!(["GET my-rsa-key"])
      |> JOSE.JWK.from_binary
    Redix.stop(conn)
    rsa_jwk
  end
end

config :my_app, MyApp.Guardian,
  allowed_algos: ["RS512"],
  secret_key: {MySecretKey, :fetch, []}

## %JOSE.JWK{} Struct ##
# Useful if you store your secret key in an encrypted JSON file with the passphrase in an environment variable.

# defined elsewhere
defmodule MySecretKey do
  def fetch do
    System.get_env("SECRET_KEY_PASSPHRASE") |> JOSE.JWK.from_file(System.get_env("SECRET_KEY_FILE"))
  end
end

config :my_app, MyApp.Guardian,
  allowed_algos: ["Ed25519"],
  secret_key: {MySecretKey, :fetch, []}

Private/Public Key pairs

A full example of how to configure guardian to use private/public key files as secrets, can be found here.

config/config.exs

config :my_app, MyApp.Guardian, issuer: “myapp”, allowed_algos: [“Ed25519”], secret_fetcher: MyApp.Guardian.KeyServer “`

Start the KeyServer in the supervision tree so it can serve requests:

”`elixir


Articles

  • coming soon...