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

# OIDC Integration

> Integrate the hosted verification flow with the standard OpenID Connect Authorization Code flow.

Hopae Connect is an OpenID Provider. You redirect the user to Hopae, Hopae runs the verification, and your backend exchanges the returned code for the verified data. Any certified OIDC client library works.

```mermaid theme={null}
sequenceDiagram
    participant U as User
    participant A as Your app
    participant H as Hopae
    A->>U: Redirect to /auth
    U->>H: Choose an eID and verify
    H->>A: Redirect to redirect_uri with code
    A->>H: POST /token (App ID + App Secret)
    H-->>A: access_token, id_token
    A->>H: GET /userinfo
    H-->>A: Verified data
```

**Before you start**

* An app's **App ID** (`client_id`) and **App Secret** (`client_secret`) from **Developers → API Settings**.
* Your redirect URI in that app's **Redirect URL Allowlist**. Matching is exact.
* At least one connection that is activated and enabled in the app's [workflow](/v2/guides/verifications/workflows).

## Endpoints

| | URL |
| :- | :- |
| Issuer (discovery, JWKS) | `https://connect.hopae.com` |
| **Authorization** | `https://connect.hopae.com/v2/auth` |
| Token | `https://connect.hopae.com/v2/token` |
| UserInfo | `https://connect.hopae.com/v2/userinfo` |

<Warning>
  Configure your library with the issuer `https://connect.hopae.com` and **override the authorization endpoint** with `/v2/auth`. Discovery lists `/auth`, which is the v1 API. The endpoint where the verification starts decides the response format, so the token and userinfo endpoints from discovery also work.
</Warning>

## 1. Redirect to /auth

```javascript theme={null}
const url = new URL("https://connect.hopae.com/v2/auth");
url.search = new URLSearchParams({
  client_id: APP_ID,
  redirect_uri: "https://app.example.com/callback",
  response_type: "code",
  scope: "openid hopae",
  state: crypto.randomUUID(),
  nonce: crypto.randomUUID(),
});
```

| Parameter | Required | Value |
| :- | :- | :- |
| `client_id` | Yes | Your App ID |
| `redirect_uri` | Yes | An allowlisted URI, exact match |
| `response_type` | Yes | `code` |
| `scope` | Yes | `openid hopae`. Without `hopae`, userinfo returns no identity data |
| `state`, `nonce` | Recommended | Random values you check on return |
| `workflow_id` | Optional | Run a workflow other than the default |
| `acr_values` | Optional | Minimum level of assurance, for example `urn:hopae:loa:3`. See [Assurance](/v2/guides/verifications/assurance) |

All parameters, including the `ui_*` display options and `private_mode`: [Authorization reference](/v2/api-reference/oidc/auth).

### Pre-select a connection

Pass `ui_connection_id` (for example `smart-id`) to skip the country and eID picker. The connection must be enabled in the workflow. Otherwise the user picks as usual. `ui_country` pre-selects only the country.

## 2. Handle the callback

```text Success theme={null}
https://app.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=b2a6c120-...&iss=https%3A%2F%2Fconnect.hopae.com
```

```text Failure theme={null}
https://app.example.com/callback?error=access_denied&error_description=Authentication%20cancelled%20by%20user&state=b2a6c120-...
```

Check `state`, and `iss` if your library supports it (`https://connect.hopae.com`). Every user-facing failure (cancel, provider error, expiry) arrives as `error=access_denied`. An unknown `workflow_id`, or `acr_values` that no enabled connection meets, arrives as `error=invalid_request`. An app without a workflow shows an error page instead of redirecting.

## 3. Exchange the code

On your backend, with your App ID and App Secret as HTTP Basic credentials (`client_secret_basic`). Form fields (`client_secret_post`) also work. Codes are single-use and expire after 5 minutes. PKCE (`S256`) is supported but optional.

```bash theme={null}
curl -X POST "https://connect.hopae.com/v2/token" \
  -u "APP_ID:APP_SECRET" \
  -d "grant_type=authorization_code" \
  -d "code=SplxlOBeZQQYbYS6WxSbIA" \
  -d "redirect_uri=https://app.example.com/callback"
```

You receive an `access_token` (10 minutes) and an `id_token` (30 minutes). No refresh token is issued. Every verification is a new authorization.

The ID token carries no personal data. Validate its signature ([JWKS](/v2/api-reference/oidc/jwks)), `iss` (`https://connect.hopae.com`), `aud` (your App ID), and `nonce`. It names the level of assurance (`acr`, `hopae_loa`) and the connection that ran (`hopae_verification.connection_id`). Details: [Token reference](/v2/api-reference/oidc/token).

## 4. Read the result

```bash theme={null}
curl "https://connect.hopae.com/v2/userinfo" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

Read identity data from `user`. Add `?provenance=true&missing_claims=true` for the audit trail and the claims the source could not provide. The payload is the same as the REST API's. See [Return Data](/v2/guides/verifications/return-data-model).

To recognise a returning person, use `user.source_id`, not `sub`. See [Return Data](/v2/guides/verifications/return-data-model#which-claims-you-receive).

## Library examples

Any certified OIDC library works: set the issuer to `https://connect.hopae.com` and override the authorization endpoint with `https://connect.hopae.com/v2/auth`. These libraries also validate the ID token for you.

<Tabs>
  <Tab title="Node.js">
    ```ts theme={null}
    import { Issuer } from "openid-client"; // v5

    const discovered = await Issuer.discover("https://connect.hopae.com");
    const issuer = new Issuer({
      ...discovered.metadata,
      authorization_endpoint: "https://connect.hopae.com/v2/auth",
    });
    const client = new issuer.Client({
      client_id: process.env.APP_ID!,
      client_secret: process.env.APP_SECRET!,
      redirect_uris: ["https://app.example.com/callback"],
      response_types: ["code"],
    });

    const authUrl = client.authorizationUrl({ scope: "openid hopae", state, nonce });
    // On callback:
    const tokenSet = await client.callback("https://app.example.com/callback", client.callbackParams(req), { state, nonce });
    const userinfo = await client.userinfo(tokenSet.access_token!);
    ```
  </Tab>

  <Tab title="Java (Spring)">
    ```yaml theme={null}
    spring.security.oauth2.client:
      registration.hopae:
        client-id: ${APP_ID}
        client-secret: ${APP_SECRET}
        authorization-grant-type: authorization_code
        scope: openid, hopae
        redirect-uri: https://app.example.com/login/oauth2/code/hopae
      provider.hopae:
        issuer-uri: https://connect.hopae.com
        authorization-uri: https://connect.hopae.com/v2/auth
    ```
  </Tab>
</Tabs>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Hosted error page instead of a redirect">
    The `redirect_uri` is not on the allowlist of the app named by `client_id`. Scheme, host, port, path, and trailing slash must match exactly.
  </Accordion>

  <Accordion title="invalid_grant at /token">
    The code expired (5 minutes) or was already used, or `redirect_uri` differs from the authorization request.
  </Accordion>

  <Accordion title="invalid_client at /token">
    Send the App ID and App Secret of the same app as HTTP Basic credentials.
  </Accordion>

  <Accordion title="userinfo has no user data">
    Add `hopae` to `scope`.
  </Accordion>

  <Accordion title="No eIDs offered on the hosted page">
    Activate a connection and enable it in the workflow you run. Production apps need their own activations.
  </Accordion>
</AccordionGroup>

<Tip>
  Native app? Open `/auth` in `ASWebAuthenticationSession` (iOS) or a Custom Tab (Android), never a WebView, and keep the token exchange on your backend. See [Mobile App Integration](/v2/guides/mobile-app-integration).
</Tip>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.