Sign In
Documentation menu
Partner-specific Last updated: 27 July 2026

Authentication

Implement the authorization-code flow, validate Parkour Design identity tokens, and manage rotating refresh tokens.

This document describes the currently implemented flow for a confidential partner application connecting to Parkour Design. Client registration and production access require technical and security approval.

Current contract: The protocol is deployed and used for coordinated integrations. It is not a self-service authorization service. Parkour Design provides the client ID, client secret, registered redirect URIs, and allowed scopes through a secure channel.

Environments and discovery

Use OpenID Connect discovery instead of constructing endpoint URLs.

EnvironmentIssuerDiscovery document
Productionhttps://app.parkour.designhttps://app.parkour.design/.well-known/openid-configuration
Testhttps://parkour-test.web.apphttps://parkour-test.web.app/.well-known/openid-configuration

Fetch discovery when the application starts, cache it for a bounded period, and refresh it when signing keys change. Require the returned issuer to exactly match the configured issuer.

The current discovery document publishes:

CapabilityCurrent value
Response typecode
Grant typesauthorization_code, refresh_token
Client authenticationclient_secret_basic, client_secret_post
ID-token signingRS256
PKCE methodsS256, plain
Scopesopenid, profile, email
Claimssub, email, email_verified, name

Use client_secret_basic and PKCE S256. Do not use plain, even though the current server advertises it for compatibility.

Registration prerequisites

Send Parkour Design the following before implementation:

  • application name and technical owner;
  • test and production redirect URIs;
  • requested scopes and user journey;
  • credential-delivery and rotation contacts;
  • logout, account unlinking, and retention requirements.

Redirect URI matching is exact. Scheme, host, port, path, letter case, and trailing slash must match the registered value. Use a callback without an existing query string or fragment.

The current service supports confidential backend clients. Browser-only and native public clients are not supported because every token request requires a client secret.

Authorization sequence

1. Create the browser transaction

Generate and store server-side:

  • a high-entropy, single-use state value;
  • a high-entropy OIDC nonce;
  • a PKCE verifier and its SHA-256 challenge;
  • the intended post-login destination;
  • a short expiration time.

Bind these values to the user’s browser session. Do not put the client secret, verifier, or tokens in a URL.

2. Redirect the user

Build the authorization URL from authorization_endpoint in discovery:

GET {authorization_endpoint}
  ?response_type=code
  &client_id={client_id}
  &redirect_uri={url_encoded_registered_redirect_uri}
  &scope={url_encoded_approved_scopes}
  &state={state}
  &nonce={nonce}
  &code_challenge={base64url_sha256_verifier}
  &code_challenge_method=S256

Use only the scopes approved during client registration. openid email profile is an example when all three scopes have been approved; requesting any unapproved scope returns invalid_scope.

Parkour Design authenticates the user and requests consent. A successful response returns:

{redirect_uri}?code={authorization_code}&state={state}

A denied request returns:

{redirect_uri}?error=access_denied&state={state}

3. Validate the callback

Before exchanging the code:

  1. reject a missing or unexpected state;
  2. reject an expired or already consumed browser transaction;
  3. consume the transaction so the callback cannot be replayed;
  4. handle an OAuth error without attempting token exchange.

Authorization codes expire after five minutes and can be used once.

4. Exchange the code server-side

Send a form-encoded request from the partner backend:

curl --request POST "${TOKEN_ENDPOINT}" \
  --user "${CLIENT_ID}:${CLIENT_SECRET}" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=${CODE}" \
  --data-urlencode "redirect_uri=${REDIRECT_URI}" \
  --data-urlencode "code_verifier=${CODE_VERIFIER}"

The redirect_uri must be identical to the value used in the authorization request.

A successful response currently has this shape:

{
  "access_token": "<signed JWT>",
  "refresh_token": "<opaque token>",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "email openid profile",
  "id_token": "<signed JWT>"
}

Token responses use Cache-Control: no-store and Pragma: no-cache. The id_token is returned only when openid is granted.

Validate identity

Validate the ID token before establishing a partner session:

  1. fetch keys from jwks_uri in discovery;
  2. select the key identified by the JWT kid;
  3. require an RS256 signature;
  4. require exact issuer equality;
  5. require the client ID as audience;
  6. validate expiration and issued-at time with a small clock-skew allowance;
  7. require the original nonce;
  8. use sub as the stable Parkour Design user identifier.

Current ID-token claims:

{
  "iss": "https://app.parkour.design",
  "sub": "<opaque Parkour Design user ID>",
  "aud": "<client ID>",
  "email": "user@example.com",
  "email_verified": true,
  "name": "Example User",
  "token_use": "id_token",
  "iat": 0,
  "exp": 0,
  "nonce": "<authorization nonce>"
}

Do not link accounts by email alone. Store iss and sub as the external identity key. Treat email and name as mutable profile attributes.

UserInfo is available when the access token contains openid:

curl "${USERINFO_ENDPOINT}" \
  --header "Authorization: Bearer ${ACCESS_TOKEN}" \
  --header "Accept: application/json"
{
  "sub": "<opaque Parkour Design user ID>",
  "email": "user@example.com",
  "email_verified": true,
  "name": "Example User"
}

Access and refresh tokens

TokenLifetimeClient handling
Authorization code5 minutesExchange once and never persist after use
ID token15 minutesValidate once; do not use as an API bearer token
Access token15 minutesKeep server-side and send only to Parkour Design resource APIs
Refresh token30 daysEncrypt at rest and replace atomically after every refresh

Refresh tokens rotate on every successful use:

curl --request POST "${TOKEN_ENDPOINT}" \
  --user "${CLIENT_ID}:${CLIENT_SECRET}" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "refresh_token=${REFRESH_TOKEN}"

The response contains a new access token and a replacement refresh token. It does not contain a new ID token. Serialize refresh operations for each user and client session, then atomically replace the stored refresh token. A reused old token returns invalid_grant.

Parkour Design consumes the old refresh token before returning its replacement. If a timeout or lost response makes the outcome unknown, do not blindly retry with the old token. Delete the local token state and start a new authorization flow. Concurrent refresh requests for the same session create the same recovery risk.

The current refresh lifetime is sliding: each successful rotation starts a new 30-day lifetime. Parkour Design does not currently expose token revocation or introspection endpoints. Disconnecting in the partner application must delete its stored tokens. An access token may remain valid until its 15-minute expiry.

Use API access tokens

Parkour Design access tokens always have the audience parkour-api. They are credentials only for the Parkour Design resource APIs described in Data exchange. Never forward them to a partner API or another third party; partner-operated resources must use their own authorization mechanism.

ID tokens establish identity and a partner session. Do not use an ID token to call any API.

Current scope behavior

The current identity scopes are openid, email, and profile. The coordinated resource API currently authorizes event and design-summary access with openid; it does not yet expose dedicated read scopes.

This is a current implementation limitation, not a pattern for new APIs. Requested scopes and accessible resources are confirmed during client registration. Future resource contracts may introduce dedicated scopes.

Current tokens and UserInfo include email and name more broadly than the individual email and profile scopes imply. Clients should still request only approved scopes and must not rely on the broader behavior remaining unchanged.

Error handling

The authorization endpoint has two error channels:

  • after a redirect URI has been validated, consent denial and some consent-processing invalid_request errors redirect to that callback with error and the original state;
  • validation failures before a trusted callback is established, including client, redirect URI, scope, response type, and PKCE failures, return a direct HTTP 400 JSON response.

Token and resource API errors also use a compact JSON body:

{"error": "invalid_grant"}
ErrorMeaning
invalid_clientMissing, unknown, inactive, or incorrectly authenticated client
invalid_redirect_uriMissing or unregistered callback URI; this is a current Parkour Design implementation-specific code
invalid_scopeEmpty or unapproved scope set
invalid_requestMissing fields or invalid PKCE combination
access_deniedUser rejected consent
invalid_grantInvalid, expired, reused, or incorrectly bound code or refresh token
unsupported_response_typeAuthorization response other than code
unsupported_grant_typeUnsupported token grant
invalid_tokenMissing, malformed, expired, or invalid API bearer token

Do not expose raw token responses or protocol diagnostics to the end user. Log a correlation ID, endpoint, status, and safe error code without logging codes, tokens, secrets, nonce values, or personal data.

Current limitations

  • Client registration and consent withdrawal are coordinated manually.
  • PKCE, state, and nonce are accepted but not enforced by the server; partner clients must enforce them.
  • Discovery advertises PKCE plain; partner clients must use S256.
  • There is no token revocation, introspection, dynamic registration, or standard end-session endpoint.
  • Refresh-token replay does not revoke an entire token family.
  • Access tokens remain valid until expiry after local logout.
  • The current resource authorization uses openid instead of dedicated resource scopes.

Review these limitations during onboarding and do not silently substitute assumptions from another OAuth provider.