Skip to content

Quickstart

This tutorial takes you from zero to a first successful login. It uses the standard authorization-code flow with PKCE and the standard claims request parameter.

You need:

  • an OIDC-capable client (any library that supports authorization code + PKCE);
  • a running application with an HTTPS callback URL (loopback http is allowed for a local development RP);
  • a back-channel logout endpoint (required at registration).

1. Register the relying party

Open the RP portal and sign in:

  • production: https://me.trustnet.app/rp/new
  • local staging: https://me.stg.trustnet.test/rp/new

The portal requires an AAL2 session and a recent step-up; you may be asked to re-authenticate before the form opens.

Fill in the registration form:

Section What to provide
Tenant Select an existing tenant or create one. A tenant groups related RPs and is the sector used for PPIDs.
Identity & consent surface The RP name (shown at consent), an optional logo, homepage/privacy/terms URLs, and contact addresses.
Claims Each claim your RP may request, with a required reason. openid is implicit and needs no reason. A claim you do not declare is never disclosed.
OAuth protocol Exact redirect URIs, post-logout redirect URIs, the required back-channel logout URI, an optional purge webhook URL, scopes and grant types.
Host verification Prove every production redirect host's registrable domain.
Operator terms Read and accept the current operator terms.

Rules that matter:

  • Redirect URIs are matched exactly. No wildcards, no fragments.
  • subject_type=pairwise and opaque access tokens are fixed by TrustNet; they are not operator-editable.
  • openid is always required. offline_access is the optional scope; request it only if you need refresh tokens.
  • authorization_code is required; add the refresh_token grant if you use refresh tokens.
  • The back-channel logout URI is required, because otherwise TrustNet cannot notify your app when a session ends or a persona changes.
  • If you set a purge webhook, it must be HTTPS and on one of your redirect hosts. It receives the signed erasure notification described in Log out.

For a local development RP (loopback only), turn on the Local development switch. It permits loopback http URLs and skips host verification, and counts against a separate five-RP limit.

Prove the production hosts

For each production redirect host, the form issues an ownership token. The registrable domain (eTLD+1) is what must be proven, so one proof covers every subdomain of that domain for your account.

  1. Add a DNS TXT record:

_trustnet-verify.<registrable-domain> TXT <token>

  1. Wait for DNS propagation and press Verify.

The management API also supports a .well-known proof method: serve the token at https://<registrable-domain>/.well-known/trustnet-client-verification and verify the proof created with method well_known. The portal form creates DNS TXT proofs.

See Prove domain ownership for detail and troubleshooting.

Save the credentials

On success the portal shows the client_id and the client_secret exactly once. Copy both now. The secret cannot be retrieved later; if it is lost, reissue it from the RP's detail page (the old secret is invalidated immediately).

2. Build the authorization request

Generate a PKCE verifier and challenge, plus random state and nonce values:

# 43–128 characters, URL-safe.
CODE_VERIFIER=$(openssl rand -base64 96 | tr -d '=+/' | cut -c1-64)
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" \
  | openssl dgst -sha256 -binary \
  | openssl base64 -A | tr '+/' '-_' | tr -d '=')
STATE=$(openssl rand -hex 16)
NONCE=$(openssl rand -hex 16)

Send the browser to the authorization endpoint. Request only claims you declared at registration; the user decides which approved claims are disclosed.

https://id.trustnet.app/oauth2/auth
  ?response_type=code
  &client_id=$CLIENT_ID
  &redirect_uri=$REDIRECT_URI
  &scope=openid%20offline_access
  &state=$STATE
  &nonce=$NONCE
  &code_challenge=$CODE_CHALLENGE
  &code_challenge_method=S256
  &claims=%7B%22id_token%22%3A%7B%22email%22%3Anull%2C%22email_verified%22%3Anull%2C%22given_name%22%3Anull%2C%22family_name%22%3Anull%2C%22picture%22%3Anull%7D%7D

The last parameter is the URL-encoded form of:

{
  "id_token": {
    "email": null,
    "email_verified": null,
    "given_name": null,
    "family_name": null,
    "picture": null
  }
}

claims is the standard OIDC request parameter. TrustNet reads it from the authorization request and shows the requested claims at consent. A null value requests a voluntary claim; "essential": true is advisory only — it never forces disclosure. TrustNet treats the id_token and userinfo targets as one request and the user's approval decides what is disclosed, so put the claims in one target and do not rely on target-specific placement.

Scopes do not select claims for new RPs. Use openid (required) and, if needed, offline_access. Older integrations that use the legacy profile, email and phone scopes continue to work on the compatibility path; declare claims for new integrations.

!!! note "Discovery and claims" The discovery document advertises claims_parameter_supported: false because the protocol layer does not interpret the parameter. TrustNet does parse it from the authorization request. Send it anyway; it is how per-claim requests work.

3. Handle the callback

The user signs in, chooses a persona, reviews the requested claims with the reasons you declared, and approves or denies. On approval, TrustNet redirects to your exact redirect URI:

https://app.example.com/callback?code=<authorization-code>&state=<state>

Verify state matches the value you sent, then exchange the code within its 10-minute lifetime:

curl -sS -X POST "https://id.trustnet.app/oauth2/token" \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "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 registered token-endpoint authentication method is client_secret_basic, which is what curl -u sends. The response has the standard token shape:

{
  "access_token": "<opaque access token>",
  "token_type": "Bearer",
  "expires_in": 1800,
  "refresh_token": "<refresh token, if offline_access was granted>",
  "id_token": "<JWT>",
  "scope": "openid offline_access"
}

Never log the token response. The access token is opaque: do not try to decode it. The ID token is a signed JWT and is the value you validate.

If the user denies the request, the callback carries error=access_denied&state=... instead of a code. Treat it as a normal cancellation, not a server error.

4. Read the PPID and the approved claims

Decode the ID token payload to inspect it (decoding is not validation — see Validate an ID token):

export ID_TOKEN='<id_token from the token response>'
python3 - <<'PY'
import base64, json, os
payload = os.environ["ID_TOKEN"].split(".")[1]
payload += "=" * (-len(payload) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
PY

A successful payload includes the standard fields plus the approved claims (values are placeholders):

{
  "iss": "https://id.trustnet.app",
  "sub": "<PPID for this RP and persona>",
  "aud": ["<client_id>"],
  "exp": 1760000000,
  "iat": 1759998200,
  "nonce": "<the nonce you sent>",
  "sid": "<session identifier>",
  "email": "<the user's approved email>",
  "email_verified": true,
  "given_name": "<the user's approved given name>"
}

Key points:

  • sub is the PPID. It is stable for your RP and the chosen persona. It is not an account identifier and you cannot correlate it with another tenant or another persona.
  • Only the claims the user approved appear. A requested claim may be absent if the chosen persona does not hold it or the user did not approve it.
  • sid is present because back-channel logout is enabled; keep it with the user's session so you can match a logout token later.

5. Call UserInfo

The OIDC UserInfo endpoint returns the approved claims for the token's persona:

curl -sS "https://id.trustnet.app/userinfo" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Server-to-server integrations can use the RP machine plane instead:

curl -sS "https://rp.trustnet.app/api/rp/v1/userinfo" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Both return sub plus the claims the user approved. The machine plane requires an active token with the openid scope and records the read as a disclosure event. See Call UserInfo.

6. What next

  • Validate the ID token properly: Validate an ID token.
  • Keep access after expiry: Refresh tokens.
  • Re-run the login as a different persona: the consent screen lets the user switch; TrustNet sends a back-channel logout to your app so you re-fetch the new persona's claims. See Log out.
  • Handle failures: Handle errors.

!!! note "Reference client" A runnable reference client lives in a separate public repository, not in this guide. The link will be published here when that repository is public.