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
httpis 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=pairwiseand opaque access tokens are fixed by TrustNet; they are not operator-editable.openidis always required.offline_accessis the optional scope; request it only if you need refresh tokens.authorization_codeis required; add therefresh_tokengrant 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.
- Add a DNS TXT record:
_trustnet-verify.<registrable-domain> TXT <token>
- 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:
subis 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.
sidis 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.