Skip to main content

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.

CredentialValue
client_idYour OAuth client identifier
client_secretYour 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 — different access model

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 against GET /.well-known/jwks.json before 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 the walletResponseUrl returned by Create Credential Offer, unchanged — its token query parameter is the authorization. The offer ID alone is not enough: a call without the right token returns 404.

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:

RoleScopes
Issuerissuance.credential.create, issuance.credential.revoke, issuance.status.update
Verifierverification.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"
}
Discover the token endpoint

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​

StatusMeaningFix
401 UnauthorizedMissing, invalid, or expired access tokenRe-acquire a token and retry; check the WWW-Authenticate header
403 ForbiddenToken is valid but lacks the scope the operation requiresRequest 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_secret in a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) — never in code or in .env files 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_secret to a client.

See Security Best Practices for the full checklist.

Next steps​