# Discovery & JWKS

> The self-description documents: /.well-known/openid-configuration (OIDC Discovery + RFC 8414) and /.well-known/jwks.json (RS256 public keys). How to verify EntryBit tokens offline.

Everything about the authorization server self-describes at two public, unauthenticated URLs. Point an OIDC library at the discovery document and it configures every endpoint itself; point a JWT verifier at the JWKS and it can validate EntryBit tokens **offline**, with no round-trip to EntryBit.

## Discovery document

```http
GET /.well-known/openid-configuration     # OIDC Discovery 1.0
GET /.well-known/oauth-authorization-server   # RFC 8414 — identical body
```

Both URLs return the same metadata. OIDC libraries look at the first; [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414) clients look at the second.

```bash
curl https://entrybit.net/.well-known/openid-configuration
```

Key fields:

| Field | Value / meaning |
|---|---|
| `issuer` | `https://entrybit.net` |
| `authorization_endpoint` | [`/api/oauth/authorize`](/docs/oauth/authorize/) |
| `token_endpoint` | [`/api/oauth/token`](/docs/oauth/token/) |
| `userinfo_endpoint` | [`/api/oauth/userinfo`](/docs/oauth/id-token-userinfo/) |
| `introspection_endpoint` | [`/api/oauth/introspect`](/docs/oauth/introspection-revocation/) |
| `revocation_endpoint` | [`/api/oauth/revoke`](/docs/oauth/introspection-revocation/) |
| `end_session_endpoint` | [`/api/oauth/logout`](/docs/oauth/logout/) |
| `jwks_uri` | `/.well-known/jwks.json` |
| `scopes_supported` | The full [scope](/docs/oauth/scopes/) catalog. |
| `response_types_supported` | `["code"]` |
| `code_challenge_methods_supported` | `["S256"]` — PKCE S256 only. |
| `subject_types_supported` | `["public"]` |
| `id_token_signing_alg_values_supported` | `["RS256"]` |
| `claims_supported` | Identity claims, plus `amr` and `auth_time`. |
| `authorization_response_iss_parameter_supported` | `true` ([RFC 9207](https://www.rfc-editor.org/rfc/rfc9207)). |

With `expo-auth-session`, `AuthSession.useAutoDiscovery('https://entrybit.net')` fetches this and wires up every endpoint — including logout — for you.

## JWKS

```http
GET /.well-known/jwks.json
```

The JSON Web Key Set: the RS256 **public** keys that verify access tokens and id_tokens. Each key's `kid` is its [RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) thumbprint — match it against the JWT header's `kid` to pick the right key.

```json
{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256",
      "kid": "<RFC 7638 thumbprint>",
      "n": "<modulus>",
      "e": "AQAB"
    }
  ]
}
```

> **Deploy note:** these are backend routes, not static files. Behind a reverse proxy, `/.well-known/openid-configuration`, `/.well-known/oauth-authorization-server`, and `/.well-known/jwks.json` must all route to the EntryBit backend.

## Verifying a token offline

EntryBit access tokens are self-contained RS256 JWTs (`typ: at+jwt`), so a resource server can authorize a request without calling EntryBit at all:

1. **Fetch and cache** the JWKS. Refresh on an unknown `kid` (keys can rotate).
2. **Select the key** whose `kid` matches the token header's `kid`.
3. **Verify the signature** with that RSA public key.
4. **Check the claims:**
   - `iss` == `https://entrybit.net`
   - `exp` in the future (and `nbf` in the past)
   - `typ` == `at+jwt` (this gate blocks access-token / id-token confusion in both directions)
   - `aud` == the expected `client_id`, if you pin it
5. **Enforce `scope`** for the operation. See [Scopes](/docs/oauth/scopes/).

Use a maintained JWT library rather than hand-rolling RSA verification. In Node.js, [`jose`](https://github.com/panva/jose) covers steps 1–4 in a few lines:

```ts
// npm install jose
import { createRemoteJWKSet, jwtVerify } from 'jose';

const ISSUER = 'https://entrybit.net';

// Step 1 — fetched once, cached, and refetched automatically when a token
// arrives signed by an unknown kid (key rotation).
const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`));

export async function verifyAccessToken(token: string, requiredScope: string) {
  // Steps 2–4 — signature, iss, exp/nbf, and typ, all enforced by jose.
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: ISSUER,
    algorithms: ['RS256'],
    typ: 'at+jwt',                   // blocks an id_token posing as an access token
    audience: 'eb_9f1c2ab34cd56ef7', // your client_id — omit if you don't pin aud
  });

  // Step 5 — enforce scope for the operation.
  const scopes = typeof payload.scope === 'string' ? payload.scope.split(' ') : [];
  if (!scopes.includes(requiredScope)) {
    throw new Error(`missing scope: ${requiredScope}`); // respond 403 insufficient_scope
  }

  return payload; // { iss, sub, aud, client_id, scope, iat, nbf, exp, jti }
}
```

```ts
// In a request handler:
const claims = await verifyAccessToken(bearerToken, 'passes:read');
// claims.sub is the EntryBit user id — authorize the operation against it.
```

If you prefer a live check over local verification — or need to inspect an opaque refresh token — a confidential client can use [Introspection](/docs/oauth/introspection-revocation/) instead.