Authentication
Credential Factory and Verification Gateway authenticate server-to-server requests with OAuth 2.0 access tokens using the client_credentials grant. You acquire a short-lived access token from the iDen2 identity provider, then send it as a Bearer token on every API request.
| Credential | Value |
|---|---|
client_id | Your OAuth client identifier |
client_secret | Your OAuth client secret |
You receive your client_id and client_secret during onboarding. Treat the secret like a password — see Keep your credentials safe.
Wallet API endpoints are called by holder wallets, not your backend. They use a different access model from the OAuth client_credentials flow above and do not use your client_secret.
- OpenID4VP (Verification Gateway): Uses JWT Authorization Request (JAR, RFC 9101). The wallet fetches a signed Request Object JWT from
GET /openid4vc/request/{sessionId}and verifies its signature againstGET /.well-known/jwks.jsonbefore responding. Integrity of the authorization request is established cryptographically, not by a bearer credential. - OID4VCI (Credential Factory): Uses capability-based access. The credential exchange itself is authorized by the pre-authorized code in the
openid-credential-offer://link. To accept or reject the offer, the wallet POSTs to thewalletResponseUrlreturned by Create Credential Offer, unchanged — itstokenquery parameter is the authorization. The offer ID alone is not enough: a call without the right token returns404.
See Mobile Wallet & Wallet API for the full holder-side flow and OpenID4VP for the JAR concept.
Scopes
Your OAuth client is provisioned with the scopes it needs. Request only the scopes for the operations you perform:
| Role | Scopes |
|---|---|
| Issuer | issuance.credential.create, issuance.credential.revoke, issuance.status.update |
| Verifier | verification.session.create, verification.session.read |
If your token is missing a scope an endpoint requires, the request returns 403 Forbidden. Contact iDen2 onboarding if you need scopes that were not provisioned for your client.
Step-by-step walkthrough
1. Acquire an access token
Request a token from the identity provider's token endpoint with the client_credentials grant. On staging:
curl -sS -X POST \
"https://keycloak.stg.iden2.app/realms/iden2-stg/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=$IDEN2_CLIENT_ID" \
-d "client_secret=$IDEN2_CLIENT_SECRET" \
-d "scope=issuance.credential.create"
The response contains a short-lived token (typically valid for ~5 minutes):
{
"access_token": "eyJhbGciOiJSUzI1Ni...",
"expires_in": 300,
"token_type": "Bearer",
"scope": "issuance.credential.create"
}
Rather than hard-coding the token URL, read it from the identity provider's discovery document — it is the authoritative source and survives realm changes:
https://keycloak.stg.iden2.app/realms/iden2-stg/.well-known/openid-configuration. The iDen2 platform team provides the issuer (realm) URL for your environment during onboarding.
2. Call the API with the token
Pass the token in the Authorization header of every request:
Authorization: Bearer <access_token>
Cache and reuse the token until shortly before expires_in elapses — do not request a fresh token per API call.
Examples
Acquire a token, then call the API. With cURL:
ACCESS_TOKEN=$(curl -sS -X POST \
"https://keycloak.stg.iden2.app/realms/iden2-stg/protocol/openid-connect/token" \
-d "grant_type=client_credentials" \
-d "client_id=$IDEN2_CLIENT_ID" \
-d "client_secret=$IDEN2_CLIENT_SECRET" \
-d "scope=issuance.credential.create" | jq -r .access_token)
curl -sS -X POST "https://iaas.stg.iden2.app/api/v1/iaas/credential-offers" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "$offer_request"
With Node.js (fetch):
async function getAccessToken() {
const res = await fetch(
'https://keycloak.stg.iden2.app/realms/iden2-stg/protocol/openid-connect/token',
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.IDEN2_CLIENT_ID,
client_secret: process.env.IDEN2_CLIENT_SECRET,
scope: 'issuance.credential.create',
}),
}
);
const { access_token } = await res.json();
return access_token;
}
const accessToken = await getAccessToken(); // cache until shortly before expiry
const response = await fetch(
'https://iaas.stg.iden2.app/api/v1/iaas/credential-offers',
{
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(offerRequest),
}
);
With Python (requests):
import os
import requests
token = requests.post(
"https://keycloak.stg.iden2.app/realms/iden2-stg/protocol/openid-connect/token",
data={
"grant_type": "client_credentials",
"client_id": os.environ["IDEN2_CLIENT_ID"],
"client_secret": os.environ["IDEN2_CLIENT_SECRET"],
"scope": "issuance.credential.create",
},
).json()["access_token"] # cache until shortly before expiry
response = requests.post(
"https://iaas.stg.iden2.app/api/v1/iaas/credential-offers",
headers={"Authorization": f"Bearer {token}"},
json=offer_request,
)
Authentication errors
| Status | Meaning | Fix |
|---|---|---|
401 Unauthorized | Missing, invalid, or expired access token | Re-acquire a token and retry; check the WWW-Authenticate header |
403 Forbidden | Token is valid but lacks the scope the operation requires | Request a token with the scope listed in Scopes; contact iDen2 if it was not provisioned |
All error responses use the RFC 9457 Problem Details format.
Keep your credentials safe
- Always use HTTPS — both for the token request and every API call.
- Store
client_id/client_secretin a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) — never in code or in.envfiles committed to version control. - Never call the APIs from a browser or mobile app — your client credentials and access tokens belong on your backend only.
- Cache tokens, don't share secrets — acquire tokens on the backend and reuse them until expiry; never ship the
client_secretto a client.
See Security Best Practices for the full checklist.