# Wiring your identity provider

> Goal: require a token exactly where the contract requires one, without writing code.

Guide - Since version 0.3.0 - Flexway v1.6.3 - https://flexway.io/en/docs/operations-identity/

> Operations guide: configure, deploy and observe the gateway. What the capability does, product side: [product guide](https://flexway.io/en/docs/product-overview/); calling it: [developer guide](https://flexway.io/en/docs/developer-overview/).

**Goal**: require a token exactly where the contract requires one, without writing code.

**Command** - the configuration says **how to verify** an identity:

```yaml
auth:
  tenant_header: X-Tenant              # where the tenant is written toward the upstream
  issuers:
    - name: staff
      discovery: https://id.example.com/realms/staff/.well-known/openid-configuration
      audience: api
      roles_claim: realm_access.roles  # Keycloak; `roles` for Entra ID
    - name: customers
      discovery: https://login.example.net/.well-known/openid-configuration
      audience: api
      tenant_claim: org
```

The **contract** says **what is required**, operation by operation - never the configuration:

```yaml
components:
  securitySchemes:
    customers: { type: openIdConnect, openIdConnectUrl: https://login.example.net/.well-known/openid-configuration }
    staff: { type: http, scheme: bearer }
paths:
  /me/orders:
    get:
      operationId: listMyOrders
      security: [ { customers: [orders:read] } ]   # a scope
  /admin/report:
    get:
      operationId: getAdminReport
      security: [ { staff: [admin] } ]             # a role (http bearer scheme)
```

A scheme's name does **not** pick the issuer: a token from **any** declared issuer satisfies the
operation when it carries the scopes or roles required. Name schemes after your issuers for the
reader's sake; trust is decided by `auth.issuers`, access by the scopes and roles.

**Check**:

| Call | Answer |
| --- | --- |
| No token | `401`, `WWW-Authenticate: Bearer` |
| Invalid, expired, wrong-issuer or wrong-audience token | `401`, `error="invalid_token"` - never `500`, and never the exact reason |
| Valid token without the scope or role | `403`, `error="insufficient_scope", scope="..."` - the scopes required; empty when only a role is missing, since a role is not an OAuth scope |
| Valid token with what is required | relayed; the upstream receives `Authorization` and `X-Tenant` from the claim |

> **Split horizon** (`0.1.0`): a provider reached under an internal name that announces its public
> one - Keycloak with a public `KC_HOSTNAME` and dynamic internal URLs - is declared with `issuer`
> beside `discovery`. Discovery and every token must state exactly that `issuer`; the local stack
> uses it (`config/dev/flexway.yaml`).
>
> ```yaml
>     - name: staff
>       discovery: http://keycloak:8080/realms/staff/.well-known/openid-configuration   # reached
>       issuer: https://login.example.com/realms/staff                                   # stated
> ```
>
> **Several audiences** (`1.0.0`): while an audience is renamed, `audiences` accepts a token issued
> for **any one** of them - the old name and the new, until the old tokens expire. Exclusive with
> `audience`; `/config` lists the accepted audiences.
>
> ```yaml
>     - name: customers
>       discovery: https://login.example.net/.well-known/openid-configuration
>       audiences: [orders-api, api]
> ```
>
> Several issuers, from different vendors: declare as many as you need. A customer often
> has one provider for employees and another for end users. The start waits for no provider:
> until its keys are loaded, its tokens get `401`, and the log says so (`issuer not reachable yet`).
>
> **Closed by default**: a header the contract does not declare does not reach the upstream, an
> undeclared query parameter is refused (`400`, naming it), a browser origin not allowed is refused
> (`403`). To allow one, a `cors` policy:

```yaml
policies:
  definitions:
    browser: { type: cors, config: { allow_origins: ["https://app.example.com"], allow_credentials: true } }
  chains:
    global: [browser]
```
