# FdsnPlugs

[![coverage report](https://gricad-gitlab.univ-grenoble-alpes.fr/OSUG/RESIF/fdsn_plugs/badges/main/coverage.svg)](https://gricad-gitlab.univ-grenoble-alpes.fr/OSUG/RESIF/fdsn_plugs/-/commits/main)
[![hex.pm](https://img.shields.io/hexpm/v/fdsn_plugs.svg)](https://hex.pm/packages/fdsn_plugs)
[![hexdocs.pm](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/fdsn_plugs/)


Collection of plugs related to FDSN webservices.

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `fdsn_plugs` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:fdsn_plugs, "~> 0.9.0"}
  ]
end
```

## Usage

This module provides two supersets of plugs: `FdsnDataselectPlugs` and
`FdsnAvailabilityPlugs` to implement dataselect and availability web services.

### Phoenix

Add the plug collection to your pipeline:

```elixir
pipeline :fdsn do
    plug :accepts, ["json", "text", "mseed"]
    plug FdsnDataselectPlugs
end
```

### JWT authentication

`FdsnPlugs.JWTAuthentication` is included in both pipeline supersets. It
authenticates requests via JWT bearer tokens, supporting multiple token
authorities.

#### 1. Start one `FdsnPlugs.JwkStrategy` per authority in your supervision tree

Each strategy fetches and caches JWKS signing keys from the given URL.

```elixir
children = [
  {FdsnPlugs.JwkStrategy,
   name: :eida,
   jwks_url: "https://geofon.gfz.de/eas2/jwk",
   time_interval: 600_000,
   first_fetch_sync: true},
  {FdsnPlugs.JwkStrategy,
   name: :other_authority,
   jwks_url: "https://auth.example.com/jwks",
   first_fetch_sync: true}
]
```

`first_fetch_sync` must be set to `true`.

##### Alternative: configuration via environment variable

`FdsnPlugs.JwkConfig.child_specs/0` reads a `JWT_AUTHORITIES` JSON array from
the environment and returns the corresponding child specs. Use it to keep the
supervision tree declarative:

```elixir
children =
  FdsnPlugs.JwkConfig.child_specs() ++ [
    # your other children…
  ]
```

The env var format is a JSON array of objects, each with a `"name"` and
`"jwks_url"`:

```bash
export JWT_AUTHORITIES='[
  {"name":"eida","jwks_url":"https://geofon.gfz.de/eas2/jwk"},
  {"name":"other","jwks_url":"https://auth.example.com/jwks","time_interval":3600000}
]'
```

If the env var is unset, empty, or malformed, `child_specs/0` returns `[]` and
no strategies are started.

#### 2. Configure which authorities the plug should try

Via application config:

```elixir
config :fdsn_plugs, :jwt_authorities, [:eida, :other_authority]
```

Via environment variable:

```elixir
config :fdsn_plugs, :jwt_authorities, FdsnPlugs.JwkConfig.authority_names()
```

Or inline in a custom pipeline:

```elixir
plug FdsnPlugs.JWTAuthentication, authorities: [:eida, :other_authority]
```

#### How it works

1. If `conn.assigns.user` is already set (e.g. HTTP Digest auth from your
   application), the plug passes through.
2. If no `Authorization` header is present, the plug passes through.
3. On a `Bearer <token>` header, each authority is tried in order.
4. On success, all token claims are stored in `conn.assigns.jwt_claims` and the
   `email` claim is stored in `conn.assigns.user`.
5. If no authority validates the token, a **401** response is returned with a
   `WWW-Authenticate` header explaining the reason.

#### Testing with curl

The token must be the **raw JWT** (`access_token` value), not the full OAuth2
JSON response. Extract it with `jq`:

```bash
ACCESS_TOKEN=$(jq -r '.access_token' token.json)
curl --oauth2-bearer "$ACCESS_TOKEN" "https://your-server/fdsnws/dataselect/1/query?net=FR&sta=RUSF"
```

Or test directly against the EIDA JWKS without a server:

```bash
ACCESS_TOKEN=$(jq -r .access_token ~/Téléchargements/eidajwt.json) mix run bin/verify_jwt.exs
```
