# OAuth 2.0 / OpenID Connect (/oauth-overview)

e-Próspera provides an OAuth 2.0 and OpenID Connect authorization server so third-party applications can authenticate users and access their data with consent.



## Capabilities [#capabilities]

* Authenticate users with their e-Próspera account
* Receive standard OIDC claims (`sub`, `name`, `email`, `picture`)
* Obtain refresh tokens for long-lived server access via `offline_access`
* Fetch user-authorized personal and legal-entity data through the `/api/v1/me/*` endpoints
* Read personal or consented-entity tax obligations and submitted filings

## Getting credentials [#getting-credentials]

OAuth clients are provisioned per account. If you already have one, you can view the **client ID**, allowed scopes, and redirect URIs in [Developer → OAuth application](https://portal.eprospera.com/developer/oauth).

To request a new client or have your secret re-issued, use the [e-Próspera Help Center chat](https://help.eprospera.com/en/chat).

## Redirect URI requirements [#redirect-uri-requirements]

Register every callback URI in full, including its scheme, host, port, path,
and query string. Authorization requests use exact string matching: a URI that
differs only by host capitalization, an explicit default port, or a trailing
slash is still a different URI.

* Production callbacks must use HTTPS.
* Staging and local-development clients may use HTTP only with `127.0.0.1`,
  `[::1]`, or `localhost`.
* Prefer `127.0.0.1` or `[::1]` for new local integrations. `localhost`
  remains supported for compatibility.
* Credentials, fragments, and raw semicolons are not permitted.
* Ports are matched exactly.

<Callout title="Testing a callback locally" type="info">
  Use a separately provisioned staging OAuth client and register the exact
  loopback callback your local application listens on, for example
  `http://127.0.0.1:3001/auth/oauth2/callback/prospera`. Start the flow through
  `https://staging-portal.eprospera.com`; the production authorization server
  rejects HTTP callbacks, including loopback addresses. See [Testing in
  staging](/testing-in-staging#oauth-in-staging) for the complete setup.
</Callout>

Older stored callbacks may remain visible while you edit a client. They can be
removed, but they are not automatically rewritten and cannot be duplicated if
they no longer satisfy the current registration policy. A grandfathered HTTP
callback is never accepted by the production authorization endpoint.

## Standard endpoints [#standard-endpoints]

| Endpoint               | URL                                                             |
| ---------------------- | --------------------------------------------------------------- |
| OpenID configuration   | `https://portal.eprospera.com/.well-known/openid-configuration` |
| JWKS                   | `https://portal.eprospera.com/api/oauth/.well-known/jwks.json`  |
| Authorization endpoint | `https://portal.eprospera.com/api/oauth/authorize`              |
| Device endpoint        | `https://portal.eprospera.com/api/oauth/device_authorization`   |
| Token endpoint         | `https://portal.eprospera.com/api/oauth/token`                  |
| UserInfo endpoint      | `https://portal.eprospera.com/api/oauth/userinfo`               |
| Revocation endpoint    | `https://portal.eprospera.com/api/oauth/revoke`                 |
| Introspection endpoint | `https://portal.eprospera.com/api/oauth/introspect`             |

Reference pages:

* [OpenID configuration](/reference/oauth-openid-configuration)
* [JWKS](/reference/oauth-jwks)
* [Authorization endpoint](/reference/oauth-authorize)
* [Device authorization endpoint](/reference/oauth-device-authorization)
* [Token endpoint](/reference/oauth-token)
* [UserInfo endpoint (GET)](/reference/oauth-userinfo)
* [UserInfo endpoint (POST)](/reference/oauth-userinfo-post)
* [Revocation endpoint](/reference/oauth-revoke)
* [Introspection endpoint](/reference/oauth-introspect)

## Recommended initial scopes [#recommended-initial-scopes]

Start with:

```text
openid profile email
```

Add `offline_access` if your backend needs refresh tokens.

## Supported scopes [#supported-scopes]

| Scope                                   | Description                                                     |
| --------------------------------------- | --------------------------------------------------------------- |
| `openid`                                | Authenticate the user with e-Próspera.                          |
| `profile`                               | Read the user's name and profile picture.                       |
| `email`                                 | Read the user's email address.                                  |
| `offline_access`                        | Receive a refresh token for background access-token renewal.    |
| `eprospera:person.details.read`         | Read detailed natural-person profile data.                      |
| `eprospera:person.residency.read`       | Read the user's current residency status.                       |
| `eprospera:person.id_verification.read` | Read the latest approved identity-verification images.          |
| `eprospera:entity.read`                 | Read legal-entity data for entities the user consents to share. |
| `eprospera:entity.documents.read`       | Read legal-entity documents for consented entities.             |
| `eprospera:person.tax.read`             | Read the user's tax obligations and submitted filings.          |
| `eprospera:entity.tax.read`             | Read tax data for consented legal entities.                     |

## Consent behavior [#consent-behavior]

When you request entity scopes (`eprospera:entity.read`,
`eprospera:entity.documents.read`, or `eprospera:entity.tax.read`), the consent
screen lets the user choose which legal entities to share. The access token is
limited to that selection.

This means:

* `GET /api/v1/me/legal-entities` can return an empty array even with a valid token.
* `GET /api/v1/me/legal-entities/{id}` and `/documents` only work for consented entity IDs.
* If the user is no longer a representative of an entity, it stops appearing in responses.

## Remembered consent [#remembered-consent]

Once a user approves your client, the grant is remembered. Later authorization requests covered by the remembered grant skip the consent screen — the user is redirected straight back to your `redirect_uri` with a fresh authorization code. Requests for additional scopes show the consent screen again, and you can always force it with `prompt=consent`.

## Revocation from Connected apps [#revocation-from-connected-apps]

Users can revoke your app's access at any time from portal **Settings → Connected apps**. Revocation invalidates all of your tokens for that user: access tokens stop working, and refresh attempts fail with `invalid_grant`. Handle `invalid_grant` by sending the user back through the authorization flow.

## PKCE [#pkce]

All newly registered clients must use PKCE: send `code_challenge` + `code_challenge_method=S256` on the authorization request and the matching `code_verifier` on the token exchange. Authorization requests from PKCE-required clients without a code challenge fail with `error=invalid_request`. Existing clients without the flag should adopt PKCE too — it is strongly recommended for every integration.

## Client secret rotation [#client-secret-rotation]

You can request a rotation of your client secret at any time through the [e-Próspera Help Center chat](https://help.eprospera.com/en/chat). After a rotation, the old secret keeps working during a grace period so you can deploy without downtime — update your integration to the new secret before the grace period expires.

## Resource endpoints [#resource-endpoints]

For an end-to-end explanation of user and entity selection, CLI examples, and
sensitive-data handling, see [Tax and legal-entity data](/tax-and-entity-data).

The scopes above unlock these endpoints:

| Endpoint                                                                                               | Required scope                          |
| ------------------------------------------------------------------------------------------------------ | --------------------------------------- |
| [GET `/api/v1/me/natural-person`](/reference/oauth-get-natural-person)                                 | `eprospera:person.details.read`         |
| [GET `/api/v1/me/natural-person/residency`](/reference/oauth-get-natural-person-residency)             | `eprospera:person.residency.read`       |
| [GET `/api/v1/me/natural-person/id-verification`](/reference/oauth-get-id-verification)                | `eprospera:person.id_verification.read` |
| [GET `/api/v1/me/legal-entities`](/reference/oauth-get-legal-entities)                                 | `eprospera:entity.read`                 |
| [GET `/api/v1/me/legal-entities/{id}`](/reference/oauth-get-legal-entities-id)                         | `eprospera:entity.read`                 |
| [GET `/api/v1/me/legal-entities/{id}/documents`](/reference/oauth-get-legal-entities-id-documents)     | `eprospera:entity.documents.read`       |
| [GET `/api/v1/me/tax/summary`](/reference/oauth-get-tax-summary)                                       | personal or entity tax read             |
| [GET `/api/v1/me/tax/filings`](/reference/oauth-list-tax-filings)                                      | personal or entity tax read             |
| [GET `/api/v1/me/tax/filings/{filingId}`](/reference/oauth-get-tax-filing)                             | personal or entity tax read             |
| [GET `/api/v1/me/tax/filings/{filingId}/documents/{document}`](/reference/oauth-download-tax-document) | personal or entity tax read             |

<Callout type="info">
  `/api/v1/me/natural-person/id_verification` (underscore) exists as a
  compatibility alias. Use `/id-verification` (hyphen) for new integrations.
</Callout>

## Authorization flow [#authorization-flow]

* `response_type=code` is required.
* `state` is required.
* `nonce` is required when `openid` is in scope.
* `response_mode` is optional (`query`, `fragment`, or `form_post`; default `query`).
* `prompt`, `max_age`, and `login_hint` are supported — see the [authorization endpoint reference](/reference/oauth-authorize).
* Every authorization response includes the `iss` parameter (RFC 9207); validate it against `https://portal.eprospera.com`.
* Unauthenticated users are redirected to the portal login before returning to the authorization request.
* Repeat the registered `redirect_uri` exactly during the authorization-code exchange.

## Client authentication and request encoding [#client-authentication-and-request-encoding]

Send token, revocation, and introspection requests as
`application/x-www-form-urlencoded`. HTTP Basic authentication is recommended:

```bash
curl -X POST https://portal.eprospera.com/api/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=$CODE" \
  -d "redirect_uri=$REDIRECT_URI" \
  -d "code_verifier=$CODE_VERIFIER"
```

`client_secret_post` remains available for compatibility. Do not combine Basic
authentication with body credentials, and do not repeat OAuth parameters.

The official CLI is a registered public client. Its device-token and revocation
requests send the client ID without a client secret. This secretless public-client
mode is reserved for the official CLI; third-party clients should use their
provisioned authentication method and the authorization-code flow with PKCE.

## Device authorization for the official CLI [#device-authorization-for-the-official-cli]

The official `eprospera` CLI is a public OAuth client and uses the device flow,
so it never embeds a client secret. Start it with:

```bash
eprospera auth login --oauth
```

The CLI displays a short code and opens the portal consent page. It polls the
token endpoint at the server-provided interval, handles `authorization_pending`
and `slow_down`, and stores the resulting rotating refresh token securely. This
flow is currently reserved for the official CLI; third-party OAuth clients use
the authorization-code flow with PKCE.

## Refresh tokens [#refresh-tokens]

Access tokens expire after 1 hour.

If the granted scope set includes `offline_access`, the token response also includes a `refresh_token` valid for up to 180 days.

Example refresh request:

```bash
curl -X POST https://portal.eprospera.com/api/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=$REFRESH_TOKEN"
```

<Callout type="warn">
  Refresh tokens rotate on every successful refresh. Always persist the newest
  `refresh_token` from the response. You may request a reduced scope set on
  refresh, provided it is a subset of the originally granted scopes. When
  `openid` is granted, the refresh response also includes a fresh `id_token`
  whose `auth_time` remains the time of the user's original authentication.
  Reusing an already rotated refresh token revokes its token family; restart the
  authorization flow after `invalid_grant`.
</Callout>

## Bearer-token errors [#bearer-token-errors]

Protected endpoints return a `WWW-Authenticate: Bearer` challenge for missing
or invalid access tokens. A valid token without the required scope returns
`403` with `error="insufficient_scope"`. The UserInfo endpoint supports both
GET and POST.

## Framework guides [#framework-guides]

* [Integration with NextAuth.js](/oauth-nextauth)
* [Integration with Firebase Authentication](/oauth-firebase)
