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

# SSO with OAuth 2.0

> Sign eToro users in to your application with OAuth 2.0 and OpenID Connect, and call the API on their behalf.

The [API key pair](/core/getting-started/authentication) authenticates you as yourself: it is generated in your own eToro settings and acts on your own account. This guide covers the other case — an application that signs **other eToro users** in and acts on their behalf, with their consent.

Use this flow whenever your application has users of its own: a trading tool, a portfolio dashboard, an agent, a partner platform. The user authenticates with eToro rather than with you, your application never handles their eToro password, and the access your application receives is limited to the scopes the user consented to and revocable by them at any time.

<Note>
  This page documents the authentication and authorization flow, which is common to every integration. Partner-specific steps that follow it — issuing a long-lived non-interactive token, associating a partner account with an eToro account, and reporting events back to eToro — are covered in the [Partner integration flow](/partners/integration).
</Note>

## Standards conformance

This is not an eToro-specific handshake. eToro's SSO service is a conformant OpenID Connect 1.0 provider built on the OAuth 2.0 authorization framework. It is therefore implemented with the same client library used for any other identity provider: the library is configured with the issuer `https://www.etoro.com` and derives its endpoints and signing keys from the discovery document.

| What you are implementing                               | Specification                                                                                                        |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Authorization code flow, token endpoint, refresh tokens | OAuth 2.0 — [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749)                                                |
| ID token, `sub`, `nonce`, pairwise subject identifiers  | [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)                                     |
| The `.well-known/openid-configuration` document         | [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html)                           |
| PKCE (`code_challenge`, `code_verifier`)                | [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)                                                            |
| JWKS key set and JWT validation                         | [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517), [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519) |
| Bearer token usage                                      | [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750)                                                            |
| Token revocation                                        | [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009)                                                            |
| Token exchange (per-client, where enabled)              | [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)                                                            |

eToro recommends the use of an established, certified client library — for example `openid-client` or `jose` on Node.js, `Microsoft.AspNetCore.Authentication.OpenIdConnect` on .NET, Spring Security OAuth2 Client on Java, or Authlib on Python. A custom protocol implementation is a common source of authentication defects, and none of the steps described below require one.

<Warning>
  **eToro does not release personal information about its users to third-party applications.** No email address, name, phone number, date of birth, or address is returned to your application unless that specific data sharing has been approved by eToro for your integration.

  Applications must therefore be designed on a token-only basis: identity is the pseudonymous `sub` claim, and authorization is the scope set on the token. See [Identity and personal data](#identity-and-personal-data).
</Warning>

## Discovery document

eToro publishes a standard OpenID Connect discovery document. Endpoint URLs and signing keys should be resolved from it at runtime rather than hardcoded: signing keys are rotated, and the document is the authoritative source.

| Resource                                 | URL                                                      |
| ---------------------------------------- | -------------------------------------------------------- |
| OpenID Connect discovery (`.well-known`) | `https://www.etoro.com/.well-known/openid-configuration` |
| JWKS (ID token signing keys)             | `https://www.etoro.com/.well-known/jwks.json`            |
| Authorization endpoint                   | `https://www.etoro.com/sso`                              |
| Token endpoint                           | `https://www.etoro.com/api/sso/v1/token`                 |
| Token revocation endpoint                | `https://www.etoro.com/api/sso/v1/token/revoke`          |
| UserInfo endpoint                        | `https://www.etoro.com/api/sso/v1/userinfo`              |

```bash theme={null}
curl -s https://www.etoro.com/.well-known/openid-configuration
curl -s https://www.etoro.com/.well-known/jwks.json
```

What the discovery document commits eToro to, and what it therefore requires of you:

<ResponseField name="issuer" type="https://www.etoro.com">
  The exact string your ID token validation must require in the `iss` claim.
</ResponseField>

<ResponseField name="response_types_supported" type="code">
  Authorization code flow only. The implicit and hybrid flows are not offered — never request `token` or `id_token` as a response type.
</ResponseField>

<ResponseField name="code_challenge_methods_supported" type="S256">
  PKCE with SHA-256. `plain` is not supported.
</ResponseField>

<ResponseField name="id_token_signing_alg_values_supported" type="RS256">
  ID tokens are signed with RS256. Reject any token whose header declares a different algorithm, and never accept `alg: none`.
</ResponseField>

<ResponseField name="subject_types_supported" type="pairwise">
  The `sub` claim is a **pairwise pseudonymous identifier** — it is specific to your client. The same eToro user presents a different `sub` to a different application, and it carries no personal information. This is the identifier to store.
</ResponseField>

<ResponseField name="grant_types_supported" type="authorization_code, refresh_token, client_credentials, urn:ietf:params:oauth:grant-type:token-exchange">
  Which of these your client may use is decided per application at registration.
</ResponseField>

<ResponseField name="token_endpoint_auth_methods_supported" type="client_secret_basic, client_secret_post, private_key_jwt, none">
  A confidential client — any application with a backend — must authenticate. Use `client_secret_basic` or `private_key_jwt`. `none` exists for public clients that cannot hold a secret.
</ResponseField>

## Prerequisites

<Steps>
  <Step title="Register your application">
    Registration produces a `clientId` and a `clientSecret`, and establishes the redirect URIs and the scope set the application may request. Register through the [eToro Builders portal](https://builders.etoro.com/app-registration), or programmatically with `POST /api/v1/sso/applications`.

    The `clientSecret` is returned **exactly once**, at creation. It is never readable again — only replaceable, via `POST /api/v1/sso/applications/{clientId}/client-secret`, which invalidates the previous secret immediately with no overlap window.
  </Step>

  <Step title="Pick your scopes">
    `GET /api/v1/sso/scopes` returns the catalog of scopes the client may request. Request the narrowest set that completes the intended user journey. Each endpoint in the [API reference](/) lists the scopes that grant access to it, and a token requires only **one** of the alternatives listed on an operation, not all of them.
  </Step>

  <Step title="Register your redirect URIs">
    Each redirect URI is matched exactly at authorization time. Register the HTTPS callback served by your backend, rather than a URL that renders the authorization code into a browser page.
  </Step>
</Steps>

## Step 1 — Redirect the user to eToro

Generate a distinct PKCE `code_verifier`, `state`, and `nonce` for each authorization attempt, retain them server-side against the browser session, and redirect the user to the authorization endpoint.

```bash theme={null}
# code_verifier: 43-128 chars of unreserved characters, cryptographically random
# code_challenge = BASE64URL(SHA256(code_verifier))

https://www.etoro.com/sso
  ?response_type=code
  &client_id=<YOUR_CLIENT_ID>
  &redirect_uri=https%3A%2F%2Fmyapp.com%2Foauth%2Fcallback
  &scope=openid%20etoro-public%3Areal%3Aread
  &state=<OPAQUE_RANDOM>
  &nonce=<OPAQUE_RANDOM>
  &code_challenge=<BASE64URL_SHA256_OF_VERIFIER>
  &code_challenge_method=S256
```

| Parameter        | Notes                                                                                                                                                                       |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `response_type`  | Always `code`.                                                                                                                                                              |
| `scope`          | Include `openid` to receive an ID token. Add the API scopes your application needs.                                                                                         |
| `state`          | Bound to the browser session and verified on the callback. This is the CSRF control for the flow: a callback presenting a `state` value you did not issue must be rejected. |
| `nonce`          | Echoed into the ID token as the `nonce` claim. Verification of this value is what prevents an ID token from another sign-in being replayed.                                 |
| `code_challenge` | Mandatory. It binds the authorization code to your token request, so an intercepted code cannot be redeemed.                                                                |

eToro authenticates the user, presents the consent screen for the scopes registered to the application, and redirects to the registered `redirect_uri` with `code` and `state`.

## Step 2 — Exchange the code for tokens

This call is made server to server from your backend, authenticated with the client credentials. The authorization code is single-use and short-lived.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://www.etoro.com/api/sso/v1/token" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -u "<YOUR_CLIENT_ID>:<YOUR_CLIENT_SECRET>" \
    -d "grant_type=authorization_code" \
    -d "code=<AUTHORIZATION_CODE>" \
    -d "redirect_uri=https://myapp.com/oauth/callback" \
    -d "code_verifier=<CODE_VERIFIER>"
  ```

  ```javascript JavaScript theme={null}
  const body = new URLSearchParams({
    grant_type: 'authorization_code',
    code,
    redirect_uri: 'https://myapp.com/oauth/callback',
    code_verifier: codeVerifier,
  });

  const res = await fetch('https://www.etoro.com/api/sso/v1/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Authorization:
        'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64'),
    },
    body,
  });

  const tokens = await res.json();
  ```
</CodeGroup>

The response contains the ID token, the access token used against the API, and a refresh token:

```json theme={null}
{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "<ACCESS_TOKEN>",
  "id_token": "<ID_TOKEN_JWT>",
  "refresh_token": "<REFRESH_TOKEN>",
  "scope": "openid etoro-public:real:read"
}
```

<Note>
  The access token authenticates API calls as `Authorization: Bearer <access_token>`. The ID token is **not** an API credential: it is an assertion of who authenticated, to be verified by your backend and then discarded or retained as a claim record. An ID token must never be sent to the API.
</Note>

## Step 3 — Validate the ID token

The ID token is the only element of this flow that identifies which user signed in, and its validation is therefore the security boundary of the integration. Retrieve the signing keys from the JWKS URI, cache them by `kid`, and refresh on an unknown `kid`.

Verify each of the following, and fail closed on any failure:

* **Signature** against the JWKS key whose `kid` matches the token header, with `alg` RS256.
* **`iss`** equals `https://www.etoro.com` exactly.
* **`aud`** contains your `clientId`.
* **`exp`** is in the future and **`iat`** is not implausibly old (allow only small clock skew).
* **`nonce`** equals the nonce you issued for this authorization attempt.
* **`sub`** is present. This is your user key.

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { createRemoteJWKSet, jwtVerify } from 'jose';

  const JWKS = createRemoteJWKSet(
    new URL('https://www.etoro.com/.well-known/jwks.json'),
  );

  const { payload } = await jwtVerify(idToken, JWKS, {
    issuer: 'https://www.etoro.com',
    audience: clientId,
    algorithms: ['RS256'],
    clockTolerance: 30,
  });

  if (payload.nonce !== expectedNonce) {
    throw new Error('ID token nonce mismatch');
  }

  // payload.sub is a pairwise pseudonymous identifier, stable for this client.
  const etoroSubject = payload.sub;
  ```

  ```bash cURL theme={null}
  # Fetch the keys; match the "kid" against the ID token's JWT header,
  # then verify RS256 with your JWT library of choice.
  curl -s https://www.etoro.com/.well-known/jwks.json
  ```
</CodeGroup>

```json theme={null}
{
  "keys": [
    {
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "kid": "00043",
      "n": "<MODULUS>",
      "e": "AQAB"
    }
  ]
}
```

<Warning>
  Verification must not be omitted on the basis that the token was received over TLS from eToro's token endpoint. Every ID token acted upon is to be validated, and a validation failure is to be treated as a failed sign-in rather than logged as a warning and disregarded.
</Warning>

Signing keys are rotated. Resolve `jwks_uri` from the discovery document, cache the key set with a short TTL, and re-fetch it when a token presents an unknown `kid`. Keys must not be pinned by value.

## Calling the API with the token

The access token is presented as `Authorization: Bearer <access_token>`, together with the `x-request-id` header required on every call:

```bash theme={null}
curl -X GET "https://public-api.etoro.com/api/v1/watchlists" \
  -H "x-request-id: <UUID>" \
  -H "Authorization: Bearer <ACCESS_TOKEN>"
```

<Warning>
  The `Authorization: Bearer` header and the `x-api-key` + `x-user-key` pair are **mutually exclusive** authentication methods. A request presenting both is rejected.
</Warning>

The scopes carried by the token determine which operations may be executed. Every operation in the API reference lists the scopes that grant access to it as alternative security requirements: a token needs **one** of the listed alternatives, not all of them, and a call whose token carries none of them is rejected.

This applies uniformly across both published documents — the [Core API](/) and the [Partners API](/partners-api-reference) — so a single token consented to the right scopes reaches every operation the application is entitled to call.

## Identity and personal data

The `sub` claim is the identity anchor for the integration. It is **pairwise**: a given eToro user presents a different `sub` value to each registered client, so the identifier is stable for your application, meaningless to anyone else, and carries no personal information.

Design around it:

* **Identity is the `sub` claim.** Use it as the primary key associating your user record with the eToro user, and resolve it on every subsequent sign-in.
* **Authorization is the scope set** on the token. Never infer entitlement from anything the user typed into your application.
* **No PII joins.** Do not build account matching, reconciliation, deduplication, or support lookups on email address, name, or phone number. Beyond being unavailable by default, matching on user-supplied attributes is an account-takeover vector: any party able to register that value in your application would inherit the corresponding eToro account.
* **A scope is permission to call, not permission to receive PII.** Where an operation would return personal data, a separate data-sharing approval governs what is returned.

Applications that require access to identity or verification data eToro already holds — the KYC and verification operations in the Partners API — are subject to an explicit per-integration approval in addition to the relevant scopes. See [Data sharing and PII](/partners/integration#data-sharing-and-pii).

## Token lifecycle

<ResponseField name="Access token" type="~1 hour">
  Refresh it with `grant_type=refresh_token` at the token endpoint, using the same client authentication as Step 2. Refresh on a `401`, and pre-emptively before expiry for long-running jobs.
</ResponseField>

<ResponseField name="Refresh token" type="long-lived">
  Store it encrypted, one per user per client. Revoke it at `https://www.etoro.com/api/sso/v1/token/revoke` when a user disconnects your application.
</ResponseField>

<ResponseField name="Client secret" type="until rotated">
  Rotate with `POST /api/v1/sso/applications/{clientId}/client-secret`. The previous secret stops working the moment the call returns, so deploy the new one in the same window.
</ResponseField>

<Note>
  An access token is bound to the user's interactive session. For backend work performed while the user is not present, a long-lived **non-interactive token** is issued instead — see [Issue a non-interactive token](/partners/integration#issue-a-non-interactive-token).
</Note>

## Security checklist

The following must be in place before an integration is promoted to production:

* PKCE `S256` on every authorization request — no exceptions, including for confidential clients.
* `state` verified against the browser session on every callback; `nonce` verified inside every ID token.
* Exact-match redirect URIs, HTTPS, pointing at your backend.
* ID token signature, `iss`, `aud`, `exp`, and `nonce` all verified, with `alg` restricted to RS256 and JWKS keys fetched by `kid`.
* Client secret and refresh tokens held in a secret store — never in source, never in a repository, never in a log line, never in an error payload, never returned to the browser.
* A working revocation path for the refresh token, wired to your own user-offboarding flow.
* `x-request-id` set to a fresh UUID on every API call, so a failure is traceable end to end across both systems.
* No PII in the account association, the logs, or the analytics.

## Reference

<CardGroup cols={2}>
  <Card title="Partner integration flow" icon="right-to-bracket" href="/partners/integration">
    Non-interactive tokens, account association, event reporting, and the data-sharing rules.
  </Card>

  <Card title="API keys and headers" icon="key" href="/core/getting-started/authentication">
    The `x-api-key` / `x-user-key` pair and request header format.
  </Card>

  <Card title="Builders portal" icon="rocket" href="https://builders.etoro.com/">
    Register your application and manage credentials.
  </Card>

  <Card title="Rate limits" icon="gauge" href="/core/getting-started/rate-limits">
    Quotas that apply to every call in this flow.
  </Card>
</CardGroup>
