README.md

# prima_auth0_ex

[![Module Version](https://img.shields.io/hexpm/v/prima_auth0_ex.svg)](https://hex.pm/packages/prima_auth0_ex)
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/prima_auth0_ex/)
[![Total Download](https://img.shields.io/hexpm/dt/prima_auth0_ex.svg)](https://hex.pm/packages/prima_auth0_ex)
[![License](https://img.shields.io/hexpm/l/prima_auth0_ex.svg)](https://github.com/primait/auth0_ex/blob/master/LICENSE.md)
[![Last Updated](https://img.shields.io/github/last-commit/primait/auth0_ex.svg)](https://github.com/primait/auth0_ex/commits/master)

An easy-to-use library to authenticate machine-to-machine communications through
Auth0.

Supports both retrieval of JWTs and their verification and validation.

## Table of contents

- [Installation](#installation)
- [Configuration](#configuration)
  - [Operational requirements](#operational-requirements)
- [Usage](#usage)
- [Development](#development)

## Installation

The package can be installed by adding `prima_auth0_ex` to your list of
dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:prima_auth0_ex, "~> 0.6.0"}
  ]
end
```

## Configuration

`prima_auth0_ex` can be configured to be used for an API consumer, an API
provider or both.

### API Consumer

To configure the library for use from a client (i.e. a service that needs to
obtain tokens to access some API), the following configuration is supported:

```elixir
config :prima_auth0_ex, :clients, default_client: [
  # Base url for Auth0 API
  auth0_base_url: "https://tenant.eu.auth0.com"
  # Credentials on Auth0
  client_id: "",
  client_secret: "",
  # Namespace for tokens of this client on the shared cache. Should be unique per client.
  cache_namespace: "my-client",
]
```

**If the client will access APIs that perform validation of permissions, make
sure that the API on Auth0 is configured to have both "Enable RBAC" and "Add
Permissions in the Access Token" enabled.** Otherwise, the JWTs generated by
Auth0 may not include the necessary permissions claims.

A visualization of the logic behind the `TokenProvider` is available
[here](client_flow.jpg).

#### Multiple clients

If you need to use multiple clients which target different Auth0 instances, you
can configure them like so

```elixir
config :prima_auth0_ex, :clients, your_client_name: [
  # Base url for Auth0 API
  auth0_base_url: "https://tenant.eu.auth0.com"
  # Credentials on Auth0
  client_id: "",
  client_secret: "",
  cache_namespace: "my-client",
]
```

You can configure multiple `:clients`, the name is arbitrary and the only
requirement is that it be unique.

### API Provider

To configure the library for use from a server (ie. a service that exposes an
API), the following configuration is supported:

```elixir
config :prima_auth0_ex, :server,
  # Base url for Auth0 API
  auth0_base_url: "https://your-tenant.eu.auth0.com/",
  # Default audience used to verify tokens. Not necessary when audience is set explicitly on usage.
  audience: "audience",
  # Issuer used to verify tokens. Can be found at https://your-tenant.eu.auth0.com/.well-known/openid-configuration
  issuer: "https://tenant.eu.auth0.com/",
  # Whether to perform the first retrieval of JWKS synchronously. Defaults to true.
  first_jwks_fetch_sync: true,
  # When true, logs errors in validation of tokens, but it does not stop the request when the token is not valid.
  # Defaults to false.
  dry_run: false,
  # When true, only the claims of tokens are validated, but their signature is not verified.
  # This is useful for local development but should NEVER be enabled on production-like systems.
  # Defaults to false.
  ignore_signature: false,
  # Level used to log requests where the authorization header is missing. 
  missing_auth_header_log_level: :warning
```

## Caching

Auth0_ex clients can cache tokens. By default the MemoryCache is used, which
should allow tokens to be shared across erlang nodes. This behavior can be
changed by setting `:prima_auth0_ex, :token_cache`

```elixir
# Disable caching of tokens
config :prima_auth0_ex, :token_cache, NoopCache
```

Note that right now caching is assumed to be all-or-nothing with respect to
multiple clients i.e. either all clients use caching or none of them do. If you
have a use case that is not supported by this please contact us so that we can
see what we can do.

### Redis

Clients can be configured to use caching through Redis. To use caching you can
use the following configuration:

```elixir
# Enables cache on redis for tokens obtained from Auth0.
config :prima_auth0_ex, :token_cache, EncryptedRedisTokenCache

config :prima_auth0_ex, :redis,
  # AES 256 key used to encrypt tokens on the shared cache.
  # Can be generated via `:crypto.strong_rand_bytes(32) |> Base.encode64()`.
  encryption_key: "uhOrqKvUi9gHnmwr60P2E1hiCSD2dtXK1i6dqkU4RTA=",
  connection_uri: "redis://redis:6379",
  # Read here for more infos: https://hexdocs.pm/redix/Redix.html#module-ssl
  ssl_enabled: false,
  ssl_allow_wildcard_certificates: false
```

Keep in mind that when saving the token, its value will be stored within a key
generated through interpolation, structured as
`#{cache_namespace}:prima_auth0_ex_tokens:#{requested_audience}`. It's important
to note that this implementation detail could potentially be subject to change
in the future.

In case a particular need emerges, you can develop a personalized iteration of
the `EncryptedRedisTokenCache` by directly applying the
`PrimaAuth0Ex.TokenCache` behavior. This involves substituting the
`config :prima_auth0_ex, :token_cache, EncryptedRedisTokenCache` configuration
with the newly crafted custom TokenCache implementation.

### DynamoDB

A new, dynamodb base caching mechanism is available. To use it you will need to
configure `ex_aws` credentials, and set a table name for auth0_ex to use. For
example:

```
config :prima_auth0_ex,
  token_cache: DynamoDB,

# See ex_aws docs
config :ex_aws,
  access_key_id: "key-id",
  secret_access_key: "secret"

config :ex_aws, :dynamodb,
  region: "eu-west-1"

config :prima_auth0_ex, :dynamodb, table_name: "prima_auth0_ex_token_cache"
```

Make sure auth0_ex has full permissions to create, read, write and update the
table.

#### Operational requirements

To cache tokens on Redis you'll need to generate a `cache_encryption_key`. This
can be done either by running `mix keygen` or by using the following snippet:

```elixir
:crypto.strong_rand_bytes(32) |> Base.encode64()
```

Alternatively you can generate it on command line (Linux/MacOSX) with:

```
dd if=/dev/random bs=1 count=32 | base64
```

> :warning: **The token needs to be 32 bytes long AND base64 encoded**, failing
> to do so will result in tokens not getting cached on Redis. :warning:

## Usage

### Obtaining tokens

Tokens for a given audience can be obtained as follows:

```elixir
{:ok, token} = PrimaAuth0Ex.token_for("target-audience")
```

This call will target the `:default_client`, assuming it's configured.

Instead, if you want to obtain a token for a specific client you can do it like
so:

```elixir
{:ok, token} = PrimaAuth0Ex.token_for("target-audience", :target_client)
```

Tokens are automatically refreshed when they expire and when the signing keys
are revoked. It is also possible to force the refresh of the token, both on the
local instance and on the shared cache, as follows:

```elixir
# With the default client
{:ok, new_token} = PrimaAuth0Ex.refresh_token_for("target-audience")

# With a specific client
{:ok, new_token} = PrimaAuth0Ex.refresh_token_for("target-audience", :target_client)
```

A use-case for forcing the refresh of the token may be e.g. if new permissions
are added to an application on Auth0, and we want to propagate this change
without waiting for the natural expiration of tokens.

### Verifying tokens

Tokens can be verified and validated as follows:

```elixir
{:ok, claims} = PrimaAuth0Ex.verify_and_validate("my-token")
```

The audience and the required permissions can be explicitly specified:

```elixir
{:ok, claims} = PrimaAuth0Ex.verify_and_validate("my-token", "my-audience", ["required-permission1"])
```

For `Plug`-based applications, a plug to automate this process is available:

```elixir
plug PrimaAuth0Ex.Plug.VerifyAndValidateToken
```

This will return `401 Forbidden` to requests without a valid bearer token.

The plug supports the following options:

- `audience: "my-audience"` to explicitly set the expected audience. When not
  defined it defaults to the audience configured in
  `:prima_auth0_ex, :server, :audience`;
- `required_permissions: ["p1", "p2"]` to forbid access to users who do not have
  all the required permissions;
- `dry_run` to allow access to the API when the token is not valid (mostly
  useful for testing purposes).

#### Validating permissions with Absinthe

To validate permissions in your Graphql API on a per-query/per-mutation basis,
an option is to define an Absinthe middleware. To this end, you can use the
[PrimaAuth0Ex.Absinthe.RequirePermissions](lib/prima_auth0_ex/absinthe/require_permissions.ex)
included with the library or build your own.

This middleware has a companion plug:
[PrimaAuth0Ex.Absinthe.CreateSecurityContext](lib/prima_auth0_ex/absinthe/create_security_context.ex),
which can be used to pass the user's permissions to the Absinthe context.

It is important to note that the middleware will only validate permissions:
other validations and the verification of the signature will still need to be
done elsewhere (e.g. using the aforementioned plug).

The middleware can be used in your schema as follows:

```elixir
field ... do
  middleware RequirePermissions, ["your-required-permission"]
  resolve &Resolver.resolve_function/3
```

### Metrics

`prima_auth0_ex` uses `:telemetry` to
[emit two events](/lib/prima_auth0_ex/token_provider/auth0_authorization_service.ex#L56)

- `[:prima_auth0_ex, :retrieve_token, :success]`
- `[:prima_auth0_ex, :retrieve_token, :failure]`

which represent a successful or failed attempt to fetch a new JWT from Auth0.

If you want to leverage them you can

- register a custom handler (see
  [here](https://hexdocs.pm/telemetry/readme.html)) or
- configure the [pre-defined handler](/lib/prima_auth0_ex/telemetry.ex)

The pre-defined handler tries to be as agnostic as possible from the underlying
reporter and it can be configured by setting the following

```elixir
config :prima_auth0_ex, telemetry_reporter: TelemetryReporter
```

At startup, the library will check if a reporter has been configured and then it
will attach it as a handler.

To work, the reporter needs to have an `increment` method like in Statix or
Instruments, and it will then increment one of two counters: `auth0.token`. Each
counter will be tagged by `audience` and `status` (`success` or `failure`).

## Development

The test suite can be executed as follows:

```bash
mix test
```

Always run formatter, linter and dialyzer before pushing changes:

```bash
mix check
```