> ## Documentation Index
> Fetch the complete documentation index at: https://docs.entire.com/llms.txt
> Use this file to discover all available pages before exploring further.

# User OAuth flow

> Sign a user in with OAuth and list the installations and repositories they can use.

A plugin can sign a user in with OAuth 2.0, then list which of its installations and repositories that user can use. Your plugin is its own OAuth client: it authenticates with the keys it already registered, and needs no client secret.

The user's token tells you who the user is and what they can reach. It doesn't let you act. To read code or report checks, you still [mint an access token for the installation](https://docs.entire.com/guides/api/plugins/act-for-an-installation.md#mint-an-access-token) with your plugin JWT.

| A user token gives you | It doesn't give you |
| :- | :- |
| The user's account ID and handle. | Email or other personal data. |
| Your installations in projects the user can read. | Any other `/api/v1` route, including repository contents. |
| The repositories in an installation the user can pull. | A way to mint access tokens. |

## What you need

* **OAuth redirect URIs.** Ask your Entire contact to add up to 10 HTTPS OAuth redirect URIs when you [register your plugin](https://docs.entire.com/guides/api/plugins/register.md), or later. Entire compares them character for character. Without them, your plugin isn't an OAuth client.
* **Your client ID**, which Entire gives you when it adds the OAuth redirect URIs. It looks like `plugin_01M5...`. It never changes, isn't a secret, and doesn't depend on your slug.
* **Your plugin's key**, the same one you sign plugin JWTs with.

## Sign the user in

Sign-in is the authorization code flow, with PKCE required. Send the user to `auth.entire.io`, which forwards them to the Core that holds their account:

```http theme={null}
GET https://auth.entire.io/authorize?response_type=code
    &client_id=plugin_01M5...
    &redirect_uri=https://ci.acme.example/oauth/callback
    &scope=openid
    &state={state}
    &code_challenge={challenge}
    &code_challenge_method=S256
```

The first time, the user approves your plugin on a consent page that shows your name and logo. Later sign-ins skip the page until the user revokes it. Core then redirects to your `redirect_uri` with `code`, `state` and `iss` ([RFC 9207](https://www.rfc-editor.org/rfc/rfc9207)). `iss` names the Core that issued the code, such as `https://eu.auth.entire.io`. Check that it's one of [Entire's Cores](https://docs.entire.com/guides/api/regions-and-routing.md).

* Core refuses a request without a PKCE challenge, and a request with a `resource` parameter.
* `scope` must be present, but Core ignores its value. Every plugin gets `openid offline_access`.
* Tie `state` to the browser session that started the sign-in, and check it on the redirect.

## Redeem the code

Redeem the code at the Core named by `iss`. `auth.entire.io` has no token endpoint. Authenticate with a client assertion ([RFC 7523](https://www.rfc-editor.org/rfc/rfc7523)) in place of a client secret:

```http theme={null}
POST {iss}/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code={code}
&redirect_uri=https://ci.acme.example/oauth/callback
&code_verifier={verifier}
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion={assertion}
```

The assertion is a JWT signed with your plugin's key:

| Claim | Value |
| :- | :- |
| `iss`, `sub` | Your client ID. |
| `aud` | The `iss` from the redirect, exactly. |
| `iat`, `exp` | At most 10 minutes apart. |

The response carries `access_token`, `id_token` and `refresh_token`. To refresh, send `grant_type=refresh_token` and the refresh token to the same Core, with a new assertion. Refresh tokens rotate: each refresh returns a new one, so store the latest.

Only the token endpoint accepts the assertion. Basic auth and a missing assertion answer `401 invalid_client`.

## What the tokens contain

| Token | Lifetime | Claims |
| :- | :- | :- |
| Access token | 4 hours | `sub` (account ID), `client_id`, `handle`, `scope`, `provider`, `provider_user_id` |
| ID token | 15 minutes | `sub`, `azp` (your client ID), `preferred_username`, `handle` |
| Refresh token | 30 days, rotating | Opaque. A refresh keeps the same scopes and claims. |

The access token's audience is `entire:plugin-api`, so it works only on the user routes below. Every other `/api/v1` route refuses it. `/userinfo` returns only `sub`, `preferred_username` and `handle`. Treat `provider` and `provider_user_id` as opaque.

## List what the user can use

Two routes take the user's access token as bearer token, on any Core. First, your active installations in projects the user can read:

```http theme={null}
GET https://{core}/api/v1/plugins/{slug}/user/installations
Authorization: Bearer {user access token}
```

```json theme={null}
{
  "installations": [{
    "id": "ins_01M4...",
    "project_id": "...",
    "project_name": "acme",
    "repository_selection": "selected",
    "created_at": "2026-10-13T20:37:23Z"
  }],
  "next_cursor": "..."
}
```

`repository_selection` is `all` or `selected`. Then the installation's repositories that the user can pull:

```http theme={null}
GET https://{core}/api/v1/plugins/{slug}/user/installations/{installation_id}/repositories
Authorization: Bearer {user access token}
```

```json theme={null}
{"repositories": [{"id": "repo_01M3...", "full_name": "acme/api"}], "next_cursor": "..."}
```

Both take `per_page`, from 1 to 100, default 30. While the response has a `next_cursor`, pass it back as `?cursor=` for the next page. The last page has no `next_cursor`. A repositories page can be short, or empty, while `next_cursor` is still set, because Entire filters each page for the user. Keep paging until there's no cursor. If the installation changes during the walk, you get `409 conflict`: start again from the first page.

The list leaves out installations the user reaches only through grants on single repositories, without read access to the project.

## Act for the user

1. Sign the user in, and list the installations and repositories they can use.
2. Let the user pick a repository from that list.
3. [Mint an access token](https://docs.entire.com/guides/api/plugins/act-for-an-installation.md#mint-an-access-token) for that installation with your plugin JWT, scoped to the repository.
4. Before each later action you take for the user, check their access through the user routes again.

## When access ends

A user's tokens can stop working before they expire, and refreshes can fail. For example:

* The user revokes your plugin. Their next sign-in shows the consent page again.
* The user's account is suspended or removed.
* Your OAuth redirect URIs are removed. New sign-ins fail too.

A `401` with detail "the account is not active" means the user lost access. Don't retry it; ask the user to sign in again.

## Which Core to call

Start every sign-in at `auth.entire.io`, not at a specific Core such as `https://eu.auth.entire.io`: it forwards the user to the Core that holds their account. Redeem and refresh at the Core named by `iss`, and set the assertion's `aud` to it. The user routes accept the token on any Core. Your client ID and keys work on every Core.

## Errors

These are for the user routes. Error bodies are RFC 9457 problem documents, with a machine-readable `code` member.

| Status and code | Meaning | What to do |
| :- | :- | :- |
| `400 bad_request` | The `cursor` or `per_page` is invalid. | Fix the query. |
| `401 unauthorized` | The token is missing or invalid, or the user lost access. | Refresh, or ask the user to sign in again. |
| `403 forbidden` | The token belongs to another plugin than `{slug}`. | Use the slug of the plugin that signed the user in. |
| `404 not_found` | The plugin is unknown, or the installation is gone, isn't yours, or is in a project the user can't read. | List the installations again. |
| `409 conflict` | The installation changed while you paged. | Start again from the first page. |
| `503 unavailable` | A dependency is down. | Retry with backoff. |
