> For the complete documentation index, see [llms.txt](https://docs.fylr.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.fylr.io/for-developers/api/oauth2.md).

# OAuth2

fylr uses OAuth2 for authentication.

User authentication in a fylr instance is done by obtaining an **Access Token** through one of the implemented OAuth2 flows. fylr is an OAuth2 **authorization server** — it is **not** an OpenID Connect provider: no `id_token` is issued. It still exposes a [UserInfo endpoint](#discovery-and-userinfo) and a [discovery document](#discovery-and-userinfo) for endpoint configuration.

{% hint style="warning" %}
**Changed in fylr 6.35.0**: fylr's OAuth2 server is now its own implementation (previously a library). The wire format, endpoints and grants are unchanged, but:

* The upgrade **logs everyone out once** — all stored access, refresh and "stay logged in" tokens are deleted. Scripts simply fetch a fresh token on their next run.
* The `openid` scope is **no longer accepted**: a request carrying it fails with `invalid_scope`. Earlier versions minted an `id_token` alongside the access token; that is gone — remove the scope from client configurations and scripts that still send it.
* `offline_access` (the OIDC-style alias for `offline`) is no longer accepted either — use the literal `offline`.
  {% endhint %}

{% hint style="info" %}
The exact request and response of every endpoint below — `/api/oauth2/auth`, `/api/oauth2/token`, `/api/oauth2/revoke`, `/api/oauth2/introspect`, `/api/oauth2/userinfo` — is also available as interactive reference panels under [API → Endpoints → /api/oauth2](/for-developers/api/endpoints/api-oauth2.md).
{% endhint %}

## Using the Access Token

After a successful login, the response contains an `access_token`. Send it with every following request to the fylr API in one of these ways:

**`Authorization` header (recommended):**

```
Authorization: Bearer <access_token>
```

**URL query parameter:**

```
?access_token=<access_token>
```

**`X-Fylr-Authorization` header** — a fallback for Safari, which can strip the `Authorization` header on cross-origin redirects:

```
X-Fylr-Authorization: Bearer <access_token>
```

An access token is valid for **24 hours** (86400 seconds) by default; the lifetime is configurable in the base configuration.

### When a token is rejected

From **fylr 6.35.0** on, a rejected access token is answered with HTTP **`401`** and the error code **`InvalidToken`** wherever a request without a token is answered `401` `UserRequired`, and on `GET /api/v1/user/session` and the web frontend's `POST /oauth2/refresh`, which answered `400` before.

A token is rejected when it is unknown, expired, revoked, bound to a different browser than the one presenting it, or belongs to a user who may no longer log in — archived, disabled, or outside their login validity window.

Two different `401`s exist, and the `code` tells them apart:

| `code`         | what happened                              | what the client should do                               |
| -------------- | ------------------------------------------ | ------------------------------------------------------- |
| `UserRequired` | the request carried no access token at all | authenticate, then retry                                |
| `InvalidToken` | the token the request carried was rejected | the session is over: discard the token and log in again |

`GET /api/v1/user/session` and `/api/oauth2/userinfo` answer `InvalidToken` also to a request without a token.

An `InvalidToken` does not become valid by being sent again, and refreshing it does not help — a refresh whose token is rejected is itself answered `401` `InvalidToken`. A client that answers this error with a retry or another refresh produces nothing but load.

Before 6.35.0 the answer depended on the endpoint: `400` `InvalidToken` from `/api/v1/user/session`, `/api/oauth2/userinfo` and `/oauth2/refresh`, `401` `UserRequired` from most others. A client that has to work against both versions should treat status `401` **or** code `InvalidToken` as the end of the session.

Endpoints that also serve callers without a user answer a request carrying a rejected token as if it carried none: deep links under `/api/v1/objects` and signed asset downloads under `/api/v1/eas/download` exactly as they always did, and endpoints that check rights instead of requiring a user with what an anonymous caller gets, such as `403` from `GET /api/v1/eas` or an empty list from `GET /api/v1/group`.

## Configuring Client ID and Secret

The descriptions of the OAuth2 flows below use `my-client` and `my-secret` as placeholders for configured Client IDs and Client Secrets. Replace these with the OAuth2 client information of your fylr instance.

Configure the pair(s) of Client ID and Secret in the [config file](/for-system-administrators/configuration/fylr.example.yml.md) `fylr.yml`:

{% code title="" %}

```yaml
fylr:
  services:
    api:
      oauth2Server:
        clients:
          my-client:
            secret: '$2y$10$64tMY.7PmUAInS2xupV2i.j1FcB7SoGlxsK3.qmuRHwyXZ2S5nlIy' # bcrypt hash of my-secret
            redirectURIs:
              - http://my-callback-server/oauth2/callback
```

{% endcode %}

The secret is stored as a **bcrypt hash**, in `fylr.yml` as in the base configuration, where at most 14 rounds are accepted; the client sends the secret itself. `htpasswd -bnBC 10 "" my-secret | tr -d ':\n'` prints such a hash.

The default clients in fylr are **public** and thus neither need nor have a secret. A public client need not send `client_secret`.

Alternatively, add the Client ID and Secret pair(s) in the [Base Configuration](/for-administrators/readme/user-management.md#oauth-service).

## Scopes

fylr understands three scopes, passed as a **space-separated** list in the `scope` parameter (e.g. `read write offline`):

| Scope           | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `offline`       | Requests a **refresh token**. Without it no refresh token is issued. Only the literal `offline` is recognised — the OIDC-style `offline_access` is rejected.                                                                                                                                                                                                                                                                                       |
| `read`, `write` | Accepted labels only — they do **not** govern API access. What a user may do is decided by fylr's per-user / per-pool **ACL rights**, not by the token scope. A token granted `read` without `write` (or neither) is just as writable as any other token today; a future release may start enforcing `read`-without-`write` as a read-only token, so request the scopes you actually intend (the fylr web frontend requests `read write offline`). |

The `openid` scope is **rejected** with `invalid_scope` since fylr 6.35.0 — fylr is not an OpenID Provider.

A client with no `scopes` configured may request any of the three; a scope outside the client's allowed set is rejected. Omitting the `scope` parameter grants no scopes — the token then works for every API call its user may make, but no refresh token is issued. The `scope` value in the token response **echoes the scopes that were granted** (for example `read write offline`), it is not a fixed value.

## OAuth2 Flows

fylr implements the **Authorization Code**, **Password**, **Client Credentials** and **Refresh Token** grants. We recommend the [Authorization Code Grant](#authorization-code-grant) (with [PKCE](#authorization-code-grant-with-pkce-code-challenge) for public clients) or the [Password Grant](#password-grant).

{% hint style="warning" %}
The **Implicit Grant** (`response_type=token`) was **removed in fylr 6.33.0**. `/api/oauth2/auth` now accepts only `response_type=code`; a request with `response_type=token` is rejected.
{% endhint %}

### Authorization Code Grant

{% hint style="info" %}
External documentation: <https://www.oauth.com/oauth2-servers/server-side-apps/authorization-code/>
{% endhint %}

This flow requires a **Client ID** (and a **Secret** for confidential clients), and a fylr **login** and **password** for each user. It offers a high level of security.

```mermaid
sequenceDiagram
    participant App as Client app
    participant Browser as User's browser
    participant fylr as fylr
    App->>Browser: open /api/oauth2/auth?response_type=code&client_id&state
    Browser->>fylr: GET /api/oauth2/auth
    fylr-->>Browser: login page
    Browser->>fylr: login + password
    fylr->>App: Step 2 — GET callback?state&code
    App->>fylr: Step 3 — POST /api/oauth2/token (code, client_id, secret)
    fylr-->>App: access_token (+ refresh_token)
```

#### **Step 1**: client calls fylr

<mark style="color:blue;">`GET`</mark> `fylr-instance/api/oauth2/auth`

**Query Parameters**

| Name                                              | Type   | Description                                                                                              |
| ------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------- |
| `response_type`<mark style="color:red;">\*</mark> | string | fixed value: `"code"`                                                                                    |
| `client_id`<mark style="color:red;">\*</mark>     | string | **Client ID**, e.g. `"my-client"`                                                                        |
| `state`<mark style="color:red;">\*</mark>         | string | Client state string (min. 8 characters), e.g. `"Authorization_Code_Grant_Login"`                         |
| `scope`                                           | string | Space-separated [scopes](#scopes). Include `offline` to receive a refresh token.                         |
| `auth_method`                                     | string | Login method (see [auth\_method](#auth_method) below). Defaults to `auto`.                               |
| `redirect_uri`                                    | string | Callback URL; must match a `redirectURIs` entry of the client. Required if the client has more than one. |

{% tabs %}
{% tab title="200 OK" %}
This redirects to the fylr login page. The user enters **login** and **password** directly into fylr.
{% endtab %}

{% tab title="400 Error" %}
Problems with the parameters, for example an invalid **Client ID**. The endpoint returns an error description explaining what is wrong.
{% endtab %}
{% endtabs %}

#### `auth_method`

`auth_method` selects the login method. It is **optional** and accepts a single value or a comma-separated list of methods, tried in order:

| Value                                | Meaning                                                                                                                                              |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `easydb`                             | Login + password against the fylr user database — the typical choice, and the fallback for unrecognized values.                                      |
| `ldap`                               | LDAP login (if an LDAP connection is configured).                                                                                                    |
| `email`, `collection`, `action_code` | Email-link, collection-share and action-code logins.                                                                                                 |
| `saml`                               | SAML single sign-on.                                                                                                                                 |
| `anonymous`                          | Guest login (requires guest access to be enabled).                                                                                                   |
| `auto`                               | The default. A server-side selector used by the web app: anonymous when guest login is enabled and no `login` is supplied, otherwise password login. |

For an interactive end-user login, use `easydb` (or omit the parameter).

#### **Step 2**: callback from fylr to the local HTTP server

This flow requires a local HTTP server that can handle the callback from fylr. Its URL must be listed in `fylr.yml` (`redirectURIs`) for the client.

fylr calls `my-callback-server/oauth2/callback` as a `GET` request with these parameters:

| Name                                      | Type   | Description                                         |
| ----------------------------------------- | ------ | --------------------------------------------------- |
| `state`<mark style="color:red;">\*</mark> | string | Client state, identical to Step 1                   |
| `code`<mark style="color:red;">\*</mark>  | string | **Authorization Code** — store it for the next step |

{% hint style="info" %}
If the callback redirects to a **cross-origin** destination, that destination must be allow-listed in `fylr.services.webapp.loginAllowRedirects` (new in 6.33.0). Same-origin destinations and the configured `externalURL` work without extra configuration.
{% endhint %}

#### **Step 3**: exchange the Authorization Code for a token

<mark style="color:green;">`POST`</mark> `fylr-instance/api/oauth2/token`

**Body Parameters** (`application/x-www-form-urlencoded`)

| Name                                           | Type   | Description                                       |
| ---------------------------------------------- | ------ | ------------------------------------------------- |
| `grant_type`<mark style="color:red;">\*</mark> | string | fixed value: `"authorization_code"`               |
| `code`<mark style="color:red;">\*</mark>       | string | **Authorization Code** from the callback          |
| `client_id`<mark style="color:red;">\*</mark>  | string | **Client ID**                                     |
| `client_secret`                                | string | **Client Secret** — omit for public clients       |
| `redirect_uri`                                 | string | Same `redirect_uri` as in Step 1, if one was used |

{% tabs %}
{% tab title="200 OK" %}
fylr returns a JSON object:

| Key             | Description                                                   |
| --------------- | ------------------------------------------------------------- |
| `access_token`  | **Access Token**                                              |
| `token_type`    | `"bearer"`                                                    |
| `scope`         | The granted scopes, e.g. `"read write offline"`               |
| `expires_in`    | Seconds until the **Access Token** expires                    |
| `refresh_token` | **Refresh Token** — only when the `offline` scope was granted |
| {% endtab %}    |                                                               |

{% tab title="400 Error" %}
Problems with the parameters, for example an invalid **Client ID** or an expired code.
{% endtab %}
{% endtabs %}

### Authorization Code Grant with PKCE Code Challenge

{% hint style="info" %}
External documentation: <https://www.oauth.com/oauth2-servers/oauth-native-apps/pkce/>
{% endhint %}

This is an extension of the [Authorization Code Grant](#authorization-code-grant) that adds a Proof Key for Code Exchange (PKCE), so a public client can use the flow securely without a client secret. All other parameters and keys are the same.

The client generates a **Code Verifier** and a **Code Challenge** per [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636):

* The **Code Verifier** is a random string of `A-Z`, `a-z`, `0-9`, `-`, `.`, `_`, `~`, 43–128 characters long.
* The **Code Challenge** is the `SHA256` hash of the Code Verifier, Base64-URL encoded.

For **Step 1**, add to the URL:

| Parameter               | Value    | Description                  |
| ----------------------- | -------- | ---------------------------- |
| `code_challenge`        |          | Generated **Code Challenge** |
| `code_challenge_method` | `"S256"` | fixed value                  |

For **Step 3**, add to the body:

| Parameter       | Value | Description                 |
| --------------- | ----- | --------------------------- |
| `code_verifier` |       | Generated **Code Verifier** |

fylr checks that the **Code Verifier** matches the **Code Challenge** from Step 1.

### Password Grant

{% hint style="info" %}
External documentation: <https://www.oauth.com/oauth2-servers/access-tokens/password-grant/>
{% endhint %}

Log directly into fylr with a user **login** and **password**.

<mark style="color:green;">`POST`</mark> `fylr-instance/api/oauth2/token`

**Body Parameters** (`application/x-www-form-urlencoded`)

| Name                                           | Type   | Description                                                              |
| ---------------------------------------------- | ------ | ------------------------------------------------------------------------ |
| `grant_type`<mark style="color:red;">\*</mark> | string | fixed value: `"password"`                                                |
| `username`<mark style="color:red;">\*</mark>   | string | fylr **login** of the user                                               |
| `password`<mark style="color:red;">\*</mark>   | string | fylr **password** of the user                                            |
| `client_id`<mark style="color:red;">\*</mark>  | string | **Client ID**                                                            |
| `client_secret`                                | string | **Client Secret** — omit for public clients                              |
| `scope`                                        | string | Space-separated [scopes](#scopes); include `offline` for a refresh token |

{% tabs %}
{% tab title="200 OK" %}
Same response as the Authorization Code Grant: `access_token`, `token_type` (`"bearer"`), `scope`, `expires_in`, plus `refresh_token` (with `offline`).
{% endtab %}

{% tab title="400 Error" %}
Invalid **Client ID**, or wrong user **login** / **password**.
{% endtab %}
{% endtabs %}

**Example (CURL)**:

```bash
curl 'fylr-instance/api/oauth2/token' \
  --data-urlencode 'grant_type=password' \
  --data-urlencode 'scope=offline' \
  --data-urlencode 'client_id=my-client' \
  --data-urlencode 'username=Login' \
  --data-urlencode 'password=Password'
```

### Refresh Token Grant

When a token response contained a `refresh_token` (i.e. the `offline` scope was granted), exchange it for a fresh access token without asking the user to log in again.

<mark style="color:green;">`POST`</mark> `fylr-instance/api/oauth2/token`

**Body Parameters** (`application/x-www-form-urlencoded`)

| Name                                              | Type   | Description                                 |
| ------------------------------------------------- | ------ | ------------------------------------------- |
| `grant_type`<mark style="color:red;">\*</mark>    | string | fixed value: `"refresh_token"`              |
| `refresh_token`<mark style="color:red;">\*</mark> | string | The refresh token                           |
| `client_id`<mark style="color:red;">\*</mark>     | string | **Client ID**                               |
| `client_secret`                                   | string | **Client Secret** — omit for public clients |

The response is a new `access_token` (and a new `refresh_token`), with the same fields as above.

### Client Credentials Grant

{% hint style="info" %}
External documentation: <https://www.oauth.com/oauth2-servers/access-tokens/client-credentials/>
{% endhint %}

{% hint style="warning" %}
The behaviour of this grant **changed in fylr 6.33.0**.
{% endhint %}

As of 6.33.0 the client-credentials grant returns a token for the **anonymous user** — so a client with a secret can obtain an anonymous session without any user credentials. A public client is refused with `invalid_grant`; the web frontend's guest login uses the authorization code flow instead. This requires **guest access** to be enabled in the base configuration (`login.guest`); with guest access off the request is rejected and a warning is written to the fylr log. Anonymous logins through this grant are recorded as `USER_LOGIN` events.

<mark style="color:green;">`POST`</mark> `fylr-instance/api/oauth2/token`

**Body Parameters** (`application/x-www-form-urlencoded`)

| Name                                              | Type   | Description                         |
| ------------------------------------------------- | ------ | ----------------------------------- |
| `grant_type`<mark style="color:red;">\*</mark>    | string | fixed value: `"client_credentials"` |
| `client_id`<mark style="color:red;">\*</mark>     | string | **Client ID**                       |
| `client_secret`<mark style="color:red;">\*</mark> | string | **Client Secret**                   |

The response contains `access_token`, `token_type` (`"bearer"`), `scope` and `expires_in`; when the `offline` scope was granted it also contains a `refresh_token` (new in 6.35.0 — earlier versions never returned one on this grant).

## Discovery and UserInfo

fylr is **not** an OpenID Connect provider — no `id_token` is issued, and the `openid` scope is rejected. Two endpoints associated with OIDC nevertheless exist, because they are useful plain-OAuth2 facilities:

**Discovery document** — a public, unauthenticated metadata document served at the server root (not under `/api`), under the path OIDC clients conventionally probe:

<mark style="color:blue;">`GET`</mark> `fylr-instance/.well-known/openid-configuration`

It advertises the issuer and the absolute URLs of the authorization, token, introspection, revocation and userinfo endpoints, plus the supported response types (`code`), grant types, scopes and claims — OAuth2 endpoint discovery in the sense of RFC 8414, so a client can configure itself automatically. It carries no id-token signing metadata. See the [discovery reference panel](/for-developers/api/endpoints/api-well-known.md).

**UserInfo endpoint** — returns claims about the user identified by the bearer access token:

<mark style="color:blue;">`GET`</mark> / <mark style="color:green;">`POST`</mark> `fylr-instance/api/oauth2/userinfo`

It requires a valid access token and returns `sub`, the user id as a string, and for a user with a login `preferred_username`, plus whatever claims (email, display name, …) are enabled in the base-config `openid.userinfo` allow-list. Identify the user by `sub`: it never changes and is never given to another user, while a login or an email can change.

## Revoking a token (Logout)

Revoke an access or refresh token ([RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009)); a `USER_LOGOUT` event is recorded for the token's user. The web app logs out through [`/logout`](#logging-out-the-browser-logout) instead.

<mark style="color:green;">`POST`</mark> `fylr-instance/api/oauth2/revoke`

**Body Parameters** (`application/x-www-form-urlencoded`)

| Name                                          | Type   | Description                                 |
| --------------------------------------------- | ------ | ------------------------------------------- |
| `token`<mark style="color:red;">\*</mark>     | string | The access or refresh token to revoke       |
| `token_type_hint`                             | string | Optional: `access_token` or `refresh_token` |
| `client_id`<mark style="color:red;">\*</mark> | string | **Client ID** the token was issued to       |
| `client_secret`                               | string | **Client Secret** — omit for public clients |

The client can authenticate with HTTP Basic instead of `client_id` and `client_secret`; without a client the request is refused with `401` `invalid_client`. Per RFC 7009 the endpoint returns `200` even for an unknown token. A token issued to another client is not revoked: the request is refused with `400` `unauthorized_client` (before fylr 6.35.0 it was answered `200`).

### Logging out the browser: `/logout`

The web app logs a user out by sending the browser to `/logout`, served at the server root (not under `/api`). The request needs no `Authorization` header: it logs out the browser that sends it, identified by its `fylr-browser-id` cookie, and then redirects.

<mark style="color:blue;">`GET`</mark> `fylr-instance/logout`

**Query Parameters**

| Name           | Type   | Description                                                                                                                                                                                                                                                                           |
| -------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `redirect`     | string | Where the browser goes after the logout, default `/`. From fylr 6.35.0 it must be a path on the fylr server (starting with `/`), a URL on `fylr.externalURL`, or a URL whose origin matches an entry of `fylr.services.webapp.loginAllowRedirects`; any other value redirects to `/`. |
| `access_token` | string | Optional: an access token to revoke as well                                                                                                                                                                                                                                           |
| `everywhere`   | string | `1` logs the user out on every browser and device, not only this one                                                                                                                                                                                                                  |

The logout

* revokes the tokens of this browser, so every tab of it is logged out (with `everywhere=1`: all tokens of the user), and records a `USER_LOGOUT` event, which sends the affected web apps back to the login,
* ends a "Keep me logged in" session and removes its cookie,
* for a user logged in via SAML whose identity provider offers single logout, sends the browser to that single logout instead of `redirect`.

The response is `303 See Other` to the redirect target.

## Token Introspection

Inspect the state of a token ([RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662)).

<mark style="color:green;">`POST`</mark> `fylr-instance/api/oauth2/introspect`

**Body Parameters** (`application/x-www-form-urlencoded`)

| Name                                      | Type   | Description                                 |
| ----------------------------------------- | ------ | ------------------------------------------- |
| `token`<mark style="color:red;">\*</mark> | string | The token to introspect                     |
| `token_type_hint`                         | string | Optional: `access_token` or `refresh_token` |

The caller authenticates with a valid access token (`Authorization: Bearer …`) or as a client with a secret, by `client_id` and `client_secret` or HTTP Basic; a public client is refused.

The response reports `active` (boolean) and, when active, `scope`, `client_id`, `sub`, `iat`, `token_type`, `iss` and `aud`, `username` when the token has one (`<anonymous>` for a guest token), and `exp` when the token expires. `sub` is the id of the token's user as a string, the same `sub` that `userinfo` returns.
