> For the complete documentation index, see [llms.txt](https://docs.kydlabs.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kydlabs.com/authentication/readme.md).

# OIDC Authorization

This guide is for KYD Labs integration partners that need an end user to authorize access to KYD APIs, especially listing partners that call the Fan API.

KYD Labs uses OAuth 2.0 Authorization Code + OpenID Connect (OIDC). PKCE is mandatory and only `S256` is supported.

## Environments

| Surface         | URL                               |
| --------------- | --------------------------------- |
| OIDC issuer     | `https://auth.kydlabs.com`        |
| Hosted login UI | `https://kydlabs.com/login/oauth` |
| Fan API         | `https://api.kydlabs.com/fans`    |

Sandbox and production are client modes, not separate domains. All clients use the same OAuth and API endpoints. The client registration controls whether issued tokens carry `mode: "sandbox"` or `mode: "production"`.

## Discovery

KYD exposes standard OIDC discovery metadata:

```http
GET https://auth.kydlabs.com/.well-known/openid-configuration HTTP/1.1
```

Discovery advertises:

* `authorization_endpoint`: `/oauth2/authorize`
* `token_endpoint`: `/oauth2/token`
* `userinfo_endpoint`: `/oauth2/userinfo`
* `jwks_uri`: `/.well-known/jwks.json`
* `response_types_supported`: `code`
* `grant_types_supported`: `authorization_code`, `refresh_token`
* `code_challenge_methods_supported`: `S256`
* `token_endpoint_auth_methods_supported`: `none`

Access tokens and ID tokens are signed with `RS256`. APIs verify tokens with the issuer JWKS.

## Authorization Flow

The production-like Fan flow is:

1. Your app redirects the user to `/oauth2/authorize` with PKCE.
2. KYD creates an authorization session and redirects the user to the hosted login UI.
3. The hosted login UI verifies the user by phone.
4. The hosted login UI asks the user to grant requested Fan actions and either all current/future tickets or selected tickets.
5. KYD redirects back to your `redirect_uri` with `code` and `state`.
6. Your backend or frontend exchanges the code at `/oauth2/token` with the original PKCE verifier.
7. Your app calls `/oauth2/userinfo` or KYD APIs with the bearer access token.

## Start Authorization

Redirect the user to:

```http
GET https://auth.kydlabs.com/oauth2/authorize HTTP/1.1
```

| Parameter                    | Description                                                                                                       |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `response_type=code`         | Required. KYD supports authorization code only.                                                                   |
| `client_id`                  | Required. OIDC client ID for your Fan API integration.                                                            |
| `redirect_uri`               | Required. Must exactly match one of the client's allowed redirect URIs.                                           |
| `scope`                      | Required. Space-delimited scopes. Every requested scope must be allowed on the client.                            |
| `state`                      | Required. Return value for CSRF protection. Validate it on callback.                                              |
| `code_challenge`             | Required. Base64URL-encoded SHA-256 challenge.                                                                    |
| `code_challenge_method=S256` | Required. Plain PKCE is not supported.                                                                            |
| `nonce`                      | Optional. Included in the ID token.                                                                               |
| `mode`                       | Optional. Must be `sandbox` or `production` if present. The client registration ultimately determines token mode. |

Example:

```http
GET https://auth.kydlabs.com/oauth2/authorize?response_type=code&client_id=demo_app&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20offline_access%20fan%3Atickets%3Aread%20fan%3Alistings%3Awrite&state=SECURE_RANDOM&code_challenge=BASE64URL_SHA256_VERIFIER&code_challenge_method=S256 HTTP/1.1
```

For regular Fan clients, KYD responds with a redirect to:

```
https://kydlabs.com/login/oauth?session_id=<id>&client_name=<name>
```

The hosted UI uses the session to handle phone verification and consent. Integrations should not call challenge or consent endpoints directly for normal user-facing Fan authorization.

## PKCE

PKCE is required on every authorization code request.

The `code_verifier` must be 43 to 128 characters and use only unreserved URI characters:

```
A-Z a-z 0-9 - . _ ~
```

The `code_challenge` is:

```
BASE64URL(SHA256(code_verifier))
```

Pass `code_challenge_method=S256` on authorize and the original `code_verifier` on token exchange.

## User Verification

For production Fan clients, the hosted UI sends an SMS challenge to the user's phone number and verifies the submitted code through KYD's verification provider.

For sandbox Fan clients, no SMS is sent. Sandbox test users have a generated phone number and static verification code. Enter that phone number and code in the hosted UI to complete login.

## Consent

After verification, the hosted UI loads consent options from the OIDC provider:

* requested actions, derived from action scopes in the authorize request
* future ticket options for the verified user

The user can grant:

* `all_current_future`: the selected actions apply to all current and future eligible tickets
* `selected`: the selected actions apply only to selected ticket IDs

Only actions requested in the original authorize scope can be granted. The granted actions are stored on the consent grant and copied into token `granted_actions`.

## Handle Callback

After consent, KYD redirects to your callback URL:

```http
GET https://app.example.com/callback?code=AUTH_CODE&state=SECURE_RANDOM HTTP/1.1
```

Validate that `state` matches the value your app generated before exchanging the code.

Authorization codes are single-use and short-lived.

## Exchange Code For Tokens

```http
POST https://auth.kydlabs.com/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
```

| Parameter                       | Description                                         |
| ------------------------------- | --------------------------------------------------- |
| `grant_type=authorization_code` | Required.                                           |
| `client_id`                     | Required.                                           |
| `code`                          | Required. Authorization code from callback.         |
| `redirect_uri`                  | Required. Must match the authorize request exactly. |
| `code_verifier`                 | Required. Original PKCE verifier.                   |

Example:

```bash
curl -X POST "https://auth.kydlabs.com/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "client_id=demo_app" \
  --data-urlencode "code=AUTH_CODE" \
  --data-urlencode "redirect_uri=https://app.example.com/callback" \
  --data-urlencode "code_verifier=ORIGINAL_PKCE_VERIFIER"
```

Example response:

```json
{
  "token_type": "Bearer",
  "expires_in": 60,
  "scope": "openid profile offline_access fan:tickets:read fan:listings:write",
  "granted_actions": ["fan:tickets:read", "fan:listings:write"],
  "grant_id": "grant_abc123",
  "grant_mode": "selected",
  "mode": "sandbox",
  "access_token": "<jwt>",
  "id_token": "<jwt>",
  "refresh_token": "<opaque-refresh-token>",
  "refresh_token_expires_in": 2592000
}
```

`refresh_token` is returned only when `offline_access` was requested and the client is allowed to request it.

## Refresh Tokens

KYD supports refresh token rotation:

```http
POST https://auth.kydlabs.com/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
```

| Parameter                  | Description                              |
| -------------------------- | ---------------------------------------- |
| `grant_type=refresh_token` | Required.                                |
| `client_id`                | Required.                                |
| `refresh_token`            | Required.                                |
| `scope`                    | Optional. Can narrow the original scope. |

Each refresh call returns a new access token and a new refresh token. Reusing a rotated refresh token revokes the refresh token family.

## Token Claims

Access tokens are JWTs with audience `kyd-api` and a short TTL. They include:

```json
{
  "sub": "user_123",
  "scope": "openid profile fan:tickets:read fan:listings:write",
  "client_id": "demo_app",
  "granted_actions": ["fan:tickets:read", "fan:listings:write"],
  "grant_id": "grant_abc123",
  "mode": "sandbox",
  "iss": "https://auth.kydlabs.com",
  "aud": "kyd-api",
  "jti": "...",
  "iat": 1782410000,
  "exp": 1782410060
}
```

ID tokens use the OAuth client ID as `aud`, expire after one hour, and include `nonce` when it was supplied on authorize.

## Userinfo

Use the access token with:

```http
GET https://auth.kydlabs.com/oauth2/userinfo HTTP/1.1
Authorization: Bearer <ACCESS_TOKEN>
```

Example response:

```json
{
  "sub": "user_123",
  "scope": "openid profile fan:tickets:read fan:listings:write",
  "granted_actions": ["fan:tickets:read", "fan:listings:write"],
  "grant_id": "grant_abc123",
  "iss": "https://auth.kydlabs.com",
  "aud": "kyd-api"
}
```

## Scopes And Actions

Standard scopes:

* `openid`
* `profile`
* `offline_access`

Fan action scopes:

* `fan:read`
* `fan:write`
* `fan:tickets:read`
* `fan:tickets:write`
* `fan:listings:read`
* `fan:listings:write`
* `fan:profile:read`
* `fan:profile:write`
* `fan:waitlist:read`
* `fan:waitlist:write`
* `fan:cart:read`
* `fan:cart:write`

Listing partners commonly request:

```
openid profile offline_access fan:tickets:read fan:listings:write
```

The `scope` string records what was requested and allowed for the client. The `granted_actions` array records what the user actually approved. KYD APIs authorize requests from `granted_actions`.

## Call KYD APIs

Call Fan API endpoints with:

```http
Authorization: Bearer <ACCESS_TOKEN>
```

For listing flows, the token must include `fan:listings:write` in `granted_actions`. To fetch ticket inventory before creating a listing authorization, request and receive `fan:tickets:read`.

`GET /tickets` is mode-aware. With a production token, KYD fetches the user's production tickets. With a sandbox token, KYD fetches tickets from the sandbox test users and permits provisioned for your integration.

For endpoint details, see `fan-api.md`.

## Browser SDK

KYD also provides `@kydlabs/browser-sdk` for browser integrations. It generates PKCE and `state`, redirects to authorize, exchanges callback codes, calls userinfo, and supports popup-based login.

## Errors

* `invalid_request` from `/oauth2/token`: verify `client_id`, `redirect_uri`, `code`, and `code_verifier`.
* `Invalid PKCE code_verifier`: recompute the challenge and confirm the verifier matches the authorize request.
* `invalid_token` from `/oauth2/userinfo`: verify issuer, expiry, audience `kyd-api`, and `Authorization: Bearer ...` formatting.
* Fan API `Unauthorized`: verify the access token is not expired and `granted_actions` includes the action required by the route.
