# OAuth and OpenID Connect endpoints

Look up every endpoint an application calls to sign people in, with its parameters, responses, scopes and errors.

Every endpoint on this page is on your project's issuer, the project's URL. The examples use `https://acme.authfi.io`. An OpenID Connect library reads all of them from the [OpenID configuration](/reference/discovery#openid-configuration).

| Endpoint                       | Use it to                                                   |
| ------------------------------ | ----------------------------------------------------------- |
| `GET /oauth/authorize`         | [Send a person to sign in](#authorize)                      |
| `POST /oauth/token`            | [Exchange a code, refresh, or sign in as a service](#token) |
| `GET /oauth/userinfo`          | [Read the signed-in person's profile](#userinfo)            |
| `POST /oauth/token/introspect` | [Ask whether an access token is active](#check-a-token)     |
| `POST /oauth/token/revoke`     | [Revoke a refresh token](#revoke-a-token)                   |
| `GET /oauth/logout`            | [End the person's session](#sign-out)                       |
| `GET /.well-known/jwks.json`   | [Fetch the keys that sign tokens](#keys)                    |

An AI agent signs in at a separate token endpoint. See [Give an AI agent an identity](/quickstart/agent).

## Authorize

Starts the authorization code flow: the browser goes here, the person signs in, and the browser returns to your application with a code.

<Endpoint method="GET" path="/oauth/authorize" />

**Authentication.** None. The browser makes this request, and the application is identified by `client_id`.

**Parameters.** Sent in the query string.

| Parameter               | Required                          | Description                                                                                 |
| ----------------------- | --------------------------------- | ------------------------------------------------------------------------------------------- |
| `client_id`             | Yes                               | Your application's client ID                                                                |
| `redirect_uri`          | Yes                               | Must match a registered redirect URI exactly. See [Application types](/applications/types). |
| `response_type`         | No                                | `code`, the only value                                                                      |
| `scope`                 | No                                | Space-separated [scopes](#scopes). `openid` if left out.                                    |
| `state`                 | No                                | A value returned to you unchanged, to match the response to the request                     |
| `code_challenge`        | For an application with no secret | The PKCE (RFC 7636) challenge. Accepted from an application with a secret.                  |
| `code_challenge_method` | No                                | `S256`, the only method accepted. `S256` if left out.                                       |
| `nonce`                 | No                                | A value returned in the ID token                                                            |
| `prompt`                | No                                | `login` for a fresh sign-in, `consent` to ask again for consent                             |
| `max_age`               | No                                | Seconds since the last sign-in that you accept. `0` means now.                              |
| `acr_values`            | No                                | The sign-in strength you require. See [`acr` values](/reference/tokens#acr-and-amr-values). |
| `login_hint`            | No                                | An account name to fill in on the sign-in page                                              |

**Example request.**

```text
https://acme.authfi.io/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.acme.com%2Fcallback&scope=openid%20profile%20email&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256
```

`$CLIENT_ID` is shown in the console when you register the application. `$STATE` and `$CHALLENGE` are values your application creates. See [Add sign-in to an app](/quickstart/sign-in).

**Example response.** The browser returns to `redirect_uri`.

```text
https://app.acme.com/callback?code=Xq3v...&state=5f2a...&iss=https%3A%2F%2Facme.authfi.io
```

**Response fields.**

| Field   | Description                                                     |
| ------- | --------------------------------------------------------------- |
| `code`  | The authorization code. Works once and expires after 5 minutes. |
| `state` | The `state` you sent, unchanged                                 |
| `iss`   | The project's issuer                                            |

**Errors.** Where the error appears depends on what is wrong.

| What is wrong                               | Response                                                                            |
| ------------------------------------------- | ----------------------------------------------------------------------------------- |
| `client_id` is unknown                      | A `401` page with `invalid_client`, shown on your project's issuer                  |
| `redirect_uri` is missing or not registered | A `400` page with `invalid_request`, shown on your project's issuer                 |
| Anything else                               | The browser returns to `redirect_uri` with `error`, `error_description` and `state` |

The browser is never sent to a URI that is not registered.

| `error` at the redirect URI         | Cause                                               |
| ----------------------------------- | --------------------------------------------------- |
| `access_denied`                     | The person is not assigned to the application       |
| `unmet_authentication_requirements` | The `acr_values` you asked for could not be reached |

Every code is listed under [Errors](#errors).

## Token

Issues tokens. One endpoint serves three grants, chosen by `grant_type`.

<Endpoint method="POST" path="/oauth/token" />

**Authentication.** An application with a client secret authenticates with HTTP Basic, or with `client_id` and `client_secret` in the body. An application without a secret sends `client_id` only.

**Parameters.** Sent form-encoded in the body. Every request carries `grant_type`, and each grant adds its own parameters.

| Parameter    | Required | Description                                                   |
| ------------ | -------- | ------------------------------------------------------------- |
| `grant_type` | Yes      | `authorization_code`, `refresh_token` or `client_credentials` |

### Authorization code grant

Exchanges the code from [Authorize](#authorize) for tokens. See [Add sign-in to an app](/quickstart/sign-in).

| Parameter       | Required                  | Description                         |
| --------------- | ------------------------- | ----------------------------------- |
| `code`          | Yes                       | The code from the redirect          |
| `redirect_uri`  | Yes                       | The same URI the code was issued to |
| `code_verifier` | When a challenge was sent | The PKCE verifier                   |

```bash
curl https://acme.authfi.io/oauth/token \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d redirect_uri=https://app.acme.com/callback \
  -d client_id="$CLIENT_ID" \
  -d code_verifier="$VERIFIER"
```

`$CODE` is the `code` from the redirect. `$VERIFIER` is the PKCE verifier whose challenge you sent to Authorize.

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6Ii4uLiJ9...",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "...",
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9...",
  "scope": "openid profile email"
}
```

### Refresh token grant

Exchanges a refresh token for new tokens. Each refresh token works once, and every use returns a new one. See [Refresh tokens](/applications/sessions#keep-a-person-signed-in).

| Parameter       | Required | Description                          |
| --------------- | -------- | ------------------------------------ |
| `refresh_token` | Yes      | The most recent refresh token        |
| `scope`         | No       | A subset of the scopes first granted |

```bash
curl https://acme.authfi.io/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token="$REFRESH_TOKEN" \
  -d client_id="$CLIENT_ID"
```

The response has the same shape as for the authorization code grant. `sid`, `amr` and `auth_time` in the new tokens are those of the original sign-in.

### Client credentials grant

Issues an access token to a backend service, for one API. Only a machine-to-machine (`m2m`) application may use it. See [Service to service](/applications/machine-to-machine).

| Parameter  | Required | Description                                                                                                 |
| ---------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `resource` | No       | The client ID of the one API the token is for. Without it, the token is for the calling application itself. |
| `scope`    | No       | Scopes to carry in the token, unchanged                                                                     |

```bash
curl https://acme.authfi.io/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d resource="$API_CLIENT_ID"
```

`$API_CLIENT_ID` is the client ID of the API's application. The calling application must hold a grant on it.

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6Ii4uLiJ9...",
  "token_type": "Bearer",
  "expires_in": 900
}
```

### Token response

**Response fields.** The response carries `Cache-Control: no-store`.

| Field           | Present                                                  | Description                                          |
| --------------- | -------------------------------------------------------- | ---------------------------------------------------- |
| `access_token`  | Always                                                   | The [access token](/reference/tokens#access-token)   |
| `token_type`    | Always                                                   | `Bearer`                                             |
| `expires_in`    | Always                                                   | Seconds until the access token expires               |
| `id_token`      | When the scope included `openid`                         | The [ID token](/reference/tokens#id-token)           |
| `refresh_token` | When a person signed in and the application may hold one | The [refresh token](/reference/tokens#refresh-token) |
| `scope`         | As in the examples above                                 | The scopes granted, space-separated                  |

The client credentials grant returns an access token only.

**Errors.**

| `error`                            | Cause                                                                                             |
| ---------------------------------- | ------------------------------------------------------------------------------------------------- |
| `invalid_grant`                    | The code expired, was already used, or the verifier does not match                                |
| `invalid_grant`                    | The refresh token was already used. Every refresh token from the same sign-in is revoked.         |
| `invalid_client`                   | The client ID is unknown, or the client secret is missing or wrong                                |
| `unauthorized_client`              | `client_credentials` from an application that is not of type `m2m`                                |
| `unsupported_grant_type`           | `grant_type` is not one of the three                                                              |
| `invalid_scope`                    | A refresh asked for more than was granted                                                         |
| `invalid_target`                   | `resource` is not an application, or the caller has no grant on it                                |
| `access_denied`                    | The person is not assigned to the application. Assignment is checked each time a token is issued. |
| `insufficient_user_authentication` | A stronger sign-in is needed before this token can be issued                                      |

## Userinfo

Returns the profile of the person an access token was issued for.

<Endpoint method="GET" path="/oauth/userinfo" />

**Authentication.** The access token, as `Authorization: Bearer`.

**Parameters.** None. `POST` is accepted too.

**Example request.**

```bash
curl https://acme.authfi.io/oauth/userinfo \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

**Example response.**

```json
{
  "sub": "ada@acme.com",
  "email": "ada@acme.com",
  "email_verified": true,
  "name": "Ada Lovelace",
  "groups": ["engineering"],
  "roles": ["developer"]
}
```

**Response fields.** The fields returned depend on the [scopes](#scopes) of the access token.

| Field                                          | Returned with scope |
| ---------------------------------------------- | ------------------- |
| `sub`                                          | Any                 |
| `email`, `email_verified`                      | `email`             |
| `name`, `picture`, `locale`, `groups`, `roles` | `profile`           |

The profile is read on each call, so a changed name shows at once. `groups` and `roles` are those in the access token.

**Errors.** None specific to this endpoint. See [Errors](#errors).

## Check a token

Tells an application whether an access token is active (token introspection).

<Endpoint method="POST" path="/oauth/token/introspect" />

**Authentication.** The client ID and client secret of an application that has a secret. The example sends them as HTTP Basic.

**Parameters.** Sent form-encoded in the body.

| Parameter | Required | Description               |
| --------- | -------- | ------------------------- |
| `token`   | Yes      | The access token to check |

**Example request.**

```bash
curl https://acme.authfi.io/oauth/token/introspect \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d token="$ACCESS_TOKEN"
```

**Example response.**

```json
{
  "active": true,
  "scope": "openid profile email",
  "client_id": "3f9c2b7e...",
  "sub": "ada@acme.com",
  "iss": "https://acme.authfi.io",
  "exp": 1760000900,
  "iat": 1760000000,
  "token_type": "Bearer"
}
```

**Response fields.**

| Field        | Description                                    |
| ------------ | ---------------------------------------------- |
| `active`     | `true` when the token is a good access token   |
| `scope`      | The scopes granted, space-separated            |
| `client_id`  | The application the token was issued to        |
| `sub`        | The person, or for a service its client ID     |
| `iss`        | The project's issuer                           |
| `exp`, `iat` | When the token expires, and when it was issued |
| `token_type` | `Bearer`                                       |

**Errors.** A token that is expired, malformed or not an access token returns `{"active": false}` and no other field.

An access token is a signed JWT, so a service can also validate it without a call. See [Checking a token](/reference/tokens#checking-a-token).

## Revoke a token

Revokes a refresh token, and with it every refresh token descended from the same sign-in.

<Endpoint method="POST" path="/oauth/token/revoke" />

**Authentication.** The application's `client_id`, in the body.

**Parameters.** Sent form-encoded in the body.

| Parameter | Required | Description                 |
| --------- | -------- | --------------------------- |
| `token`   | Yes      | The refresh token to revoke |

**Example request.**

```bash
curl https://acme.authfi.io/oauth/token/revoke \
  -d token="$REFRESH_TOKEN" \
  -d client_id="$CLIENT_ID"
```

**Example response.** `200` with an empty body, whether or not the token existed.

**Errors.** None specific to this endpoint. See [Errors](#errors).

This endpoint acts on refresh tokens. An access token stays valid until it expires, which is why access tokens last 15 minutes by default. To end a person's access sooner, end their session or remove their access. See [Sign out](/applications/sessions#sign-a-person-out).

## Sign out

Ends the person's session and removes its cookie. This is the end-session endpoint.

<Endpoint method="GET" path="/oauth/logout" />

**Authentication.** None. The browser makes this request.

**Parameters.** Sent in the query string.

| Parameter                  | Required | Description                                                   |
| -------------------------- | -------- | ------------------------------------------------------------- |
| `client_id`                | No       | Your application's client ID. Needed to send the person back. |
| `post_logout_redirect_uri` | No       | Where to send the person afterwards                           |
| `state`                    | No       | A value returned to you unchanged                             |

**Example request.**

```text
https://acme.authfi.io/oauth/logout?client_id=$CLIENT_ID&post_logout_redirect_uri=https%3A%2F%2Fapp.acme.com%2F&state=abc
```

**Response.** The outcome depends on the parameters.

| Request                                                                                                   | The person                             |
| --------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| `client_id` is given, and `post_logout_redirect_uri` has the scheme and host of a registered redirect URI | Returns to `post_logout_redirect_uri`  |
| Anything else                                                                                             | Sees a page saying they are signed out |

**Errors.** None specific to this endpoint.

To also stop the refresh token your application holds, [revoke it](#revoke-a-token). See [Sign out](/applications/sessions#sign-a-person-out).

## Keys

Returns the project's public signing keys as a JWK Set (RFC 7517).

<Endpoint method="GET" path="/.well-known/jwks.json" />

**Authentication.** None.

**Parameters.** None.

**Example request.**

```bash
curl https://acme.authfi.io/.well-known/jwks.json
```

**Response.** The set holds one `RS256` key and one `EdDSA` (Ed25519) key.

| Key     | Signs                                               |
| ------- | --------------------------------------------------- |
| `RS256` | ID tokens and access tokens for people and services |
| `EdDSA` | Access tokens and transaction tokens for AI agents  |

Each token names its key in the `kid` header. Keys rotate, and a rotated key stays published for 24 hours. Fetch the set when you meet a `kid` you do not know, not on every request, and do not pin one key. See [Keys and secrets](/security/keys).

**Errors.** None specific to this endpoint.

## Scopes

A scope is a name your application sends in `scope` to say what it wants. Send several separated by spaces.

| Scope     | Purpose                         | Adds at userinfo                               |
| --------- | ------------------------------- | ---------------------------------------------- |
| `openid`  | Required for an ID token        | —                                              |
| `profile` | The person's profile and access | `name`, `picture`, `locale`, `groups`, `roles` |
| `email`   | The person's email              | `email`, `email_verified`                      |

Userinfo returns `sub` with any scope. `profile` and `email` decide what else [userinfo](#userinfo) returns. The ID token carries `email`, `email_verified`, `name`, `groups` and `roles` whatever scopes you request. See [ID token](/reference/tokens#id-token).

## Errors

Every endpoint on this page reports a failure as OAuth 2.0 defines (RFC 6749): a code in `error`, and sometimes a sentence in `error_description`.

```json
{ "error": "invalid_grant", "error_description": "authorization code is invalid or expired" }
```

The authorization endpoint sends the same two fields, with `state`, as query parameters on the redirect URI.

| `error`                             | Status | Meaning                                                      |
| ----------------------------------- | ------ | ------------------------------------------------------------ |
| `invalid_request`                   | `400`  | A parameter is missing or malformed                          |
| `invalid_client`                    | `401`  | The client is unknown, or its secret is wrong                |
| `invalid_grant`                     | `400`  | The code or refresh token is wrong, expired or already used  |
| `unauthorized_client`               | `400`  | This application may not use this grant                      |
| `unsupported_grant_type`            | `400`  | The grant type is not one the endpoint offers                |
| `invalid_scope`                     | `400`  | A refresh asked for more than was granted                    |
| `invalid_target`                    | `400`  | `resource` is not something this caller may address          |
| `access_denied`                     | `403`  | The person is not assigned to the application                |
| `login_required`                    | `401`  | The session is too old for `prompt=login` or `max_age`       |
| `interaction_required`              | `401`  | The person must complete another step                        |
| `unmet_authentication_requirements` | `400`  | The `acr_values` asked for could not be reached              |
| `insufficient_user_authentication`  | `401`  | A stronger sign-in is needed before this token can be issued |
| `temporarily_unavailable`           | `503`  | Try again shortly                                            |
| `forbidden`                         | `403`  | The project's plan or account does not allow this            |
| `rate_limited`                      | `429`  | Too many attempts                                            |

For what to do about each code, see [Errors](/reference/errors#oauth-endpoints-for-applications).
