Skip to content

Client & API

Build a two-part application: a client app that obtains user consent via OAuth2, and a resource API that validates access tokens and enforces scopes. This demonstrates the full three-party OAuth2 architecture.

Separating the client and API into two services is the standard OAuth2 architecture for third-party integrations. The client never sees the user’s Paylent password — it only receives an access token with specific scopes. The API doesn’t trust the client directly; it validates the JWT signature against Paylent’s public keys via JWKS. This means the API can verify tokens without making a network call to Paylent on every request.

Custom scopes like read:documents and write:documents give you fine-grained control over what each client can do. When the user logs in, they see exactly what permissions the app is requesting and can make an informed decision. This is the same consent model used by GitHub, Google, and other OAuth providers — and it’s the right pattern whenever a third-party app needs to act on behalf of a user.

  • A client (port 3000) that authenticates users via Authorization Code + PKCE and calls the API with access tokens
  • A resource API (port 4100) that validates JWT tokens against Paylent’s JWKS endpoint and checks scopes

This example needs a confidential OAuth client and two custom scopes. Here’s how to set them up:

  1. Register an OAuth client

    Open the dashboard at http://dashboard.localhost:4000, select the examples-dev environment, and navigate to Clients. Click Register Application and fill in:

    • Name: My Documents App
    • Client Type: Confidential
    • Grant Types: Authorization Code, Refresh Token
    • Redirect URIs: http://localhost:3000/callback

    Client & API client details

    Copy the client secret and add it to the example’s .env file.

  2. Create custom scopes

    Navigate to Scopes in the sidebar. You’ll see the standard OIDC scopes (openid, profile, email). Click Create Scope to add two custom scopes:

    • read:documents — Read documents
    • write:documents — Write documents

    Scopes list with custom scopes

    These scopes are what the client requests during authorization, and what the API checks when handling requests.

    Create scope form

  1. Install dependencies

    Terminal window
    cd examples/client-and-api
    npm install
  2. Start both servers

    Terminal window
    npm run dev

    This starts:

    • Client at http://localhost:3000
    • API at http://localhost:4100
  3. Open the client and log in

    Go to http://localhost:3000. You’ll see “Third-Party Documents App” with a login button.

    Client home page

    Click Log in with Paylent. You’re redirected to Paylent’s login page. Enter:

    Paylent login page

  4. Browse the app

    After login, you’ll see a menu with links:

    • View your profile — Calls GET /api/profile on the resource API, which requires the profile scope
    • View your documents — Calls GET /api/documents, which requires the read:documents scope

    Authenticated menu

The client implements the OAuth2 Authorization Code flow with PKCE manually:

// Generate PKCE challenge
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Redirect to Paylent's authorize endpoint
const authUrl = new URL(`${ISSUER}/oauth/authorize`);
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("client_id", CLIENT_ID);
authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
authUrl.searchParams.set("scope", "openid profile read:documents");
authUrl.searchParams.set("code_challenge", codeChallenge);
authUrl.searchParams.set("code_challenge_method", "S256");

On the callback, the client exchanges the authorization code for tokens:

const tokenResponse = await fetch(`${ISSUER}/oauth/token`, {
method: "POST",
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code,
code_verifier: codeVerifier,
redirect_uri: REDIRECT_URI,
}),
});

The API validates access tokens by fetching the public keys from Paylent’s JWKS endpoint:

// Fetch JWKS from Paylent (cached for 5 minutes)
const jwksResponse = await fetch(`${ISSUER}/.well-known/jwks.json`);
const jwks = await jwksResponse.json();
// Verify the JWT signature and claims
const publicKey = await importJWK(matchingKey, "RS256");
const { payload } = await jwtVerify(token, publicKey);

Each endpoint checks that the token includes the required scope:

app.get("/api/documents", requireScope("read:documents"), (c) => {
// Only accessible with a token that has "read:documents" scope
});
  • CLI Auth — Authenticate from a CLI tool using the same PKCE flow
  • OAuth Endpoints — Full reference for the token, authorize, and introspect endpoints