OAuth 2.0 + PKCE

Sign in with AIVerID

Integrate secure authentication and AI rights management into your applications with our OAuth 2.0 API

πŸš€ Try OAuth Console Get API Access

Why Choose AIVerID OAuth?

πŸ”

Enterprise Security

OAuth 2.0 with PKCE flow, AES-256-GCM encryption, and zero-knowledge architecture ensure maximum security for your users.

⚑

Easy Integration

Simple REST API with comprehensive documentation and SDKs for popular programming languages.

🌐

Global AI Rights

Access user's AI rights tokens (AIVID, AIPLT, SLIVR) and verified identity information with consent.

Quick Start Example

const AIVERID = 'https://aiverid-backend-production.up.railway.app';

// 1. Send the user to the authorization endpoint
const authUrl = new URL(`${AIVERID}/oauth/authorize`);
authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.append('redirect_uri', 'YOUR_CALLBACK_URL');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('scope', 'profile email');
authUrl.searchParams.append('state', generateRandomState());
// Optional: require a verified identity for this flow (1-4).
// The user is prompted to step up before consent is granted.
// authUrl.searchParams.append('min_identity_level', '2');

window.location.href = authUrl.toString();

// 2. Exchange the code for tokens (server-side β€” never in the browser)
const code = new URLSearchParams(window.location.search).get('code');
const response = await fetch(`${AIVERID}/oauth/token`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    code,
    client_id: 'YOUR_CLIENT_ID',
    client_secret: 'YOUR_CLIENT_SECRET', // omit for public clients; send code_verifier instead
    redirect_uri: 'YOUR_CALLBACK_URL'
  })
});
const { access_token, refresh_token } = await response.json();

// 3. Read the profile
const me = await fetch(`${AIVERID}/oauth/userinfo`, {
  headers: { Authorization: `Bearer ${access_token}` }
}).then(r => r.json());

// me.sub / me.aiverid β†’ the member number (opaque string, e.g. "TH...")
// me.picture          β†’ profile photo URL (not me.avatar_url)
// me.verification_level β†’ 1 Basic Β· 2 Verified Β· 3 Approved Β· 4 Certified

Callback URLs

Your redirect_uri is compared by exact string match β€” no host aliasing, no path normalisation, no trailing-slash forgiveness. Register and send the canonical host your app actually serves, character for character.

This is the integration bug that costs the most time, because it fails after a successful authorization and looks like a broken state check: register https://www.example.xyz/callback while your app canonicalises www to the apex, and the browser lands on the redirect, gets bounced to the apex, and arrives at your callback without the session cookie you set on the apex β€” so your own state comparison fails and loops. Nothing in the AIVerID response says "host mismatch", and nothing can: the hub redirected to precisely the URI you registered. Check the canonical host first whenever a callback reports an invalid or missing state.

If your app answers on both hosts, register both β€” but still send the canonical one. Give each distinct flow its own callback path (/oauth/callback for sign-in, a separate one for any agent or elevated-scope grant) so the two can never share state or be confused in a log.

Signing Out (required)

Clearing your own session is not enough β€” the tokens AIVerID issued you stay valid until they expire. Your sign-out MUST also revoke them (RFC 7009). Revoke each token separately: revocation matches an exact token, so the access token and the refresh token need one call each. The examples here post JSON; the endpoint also accepts standard application/x-www-form-urlencoded bodies, so a generic RFC 7009 client library works unmodified.

Two rules that are easy to get wrong: session cookies must be cleared server-side β€” document.cookie cannot touch an httpOnly cookie and fails silently, leaving a "signed-out" user fully signed in; and never send your client secret from browser code β€” revocation accepts client_id + token without a secret, precisely so public and browser-side clients can call it.

// server-side route β€” e.g. POST /api/auth/logout
// async so a synchronous throw becomes a rejection allSettled can absorb
const revoke = async (token) =>
  fetch('https://aiverid-backend-production.up.railway.app/oauth/revoke', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ client_id: CLIENT_ID, token }),  // no secret needed
  })

// best-effort: an AIVerID outage must never trap a user in a session
await Promise.allSettled([revoke(accessToken), revoke(refreshToken)])

// then clear YOUR session cookies here, server-side, whatever happened above

Three failure modes found in real Ver* integrations β€” check all of them before calling sign-out done:

This ends the session for your application only. Users who want to sign out of everything at once can do it from their AIVerID dashboard β€” every connected app can be revoked individually, or all at once with Sign out everywhere.

Agent Tokens (for services opening an agent API)

Skip this section unless you are exposing an API that an AI agent calls on a user's behalf. Ordinary sign-in needs nothing here.

AIVerID issues a distinct kind of access token for a delegation: the user names an agent, approves a narrow set of scopes, and gets one revoke button per agent in their dashboard β€” killing that agent without touching their own session or any other agent. Your job at the other end is to accept only those tokens on your agent API.

The rule: fail closed on all four

Accept a request on your agent surface only when every one of these holds at once. Any single one missing is a refusal:

And three things you must never do: never accept a token with no aud β€” no "missing means fine" fallback, that gap is the entire reason this exists; never treat scope alone as proof of agency; and never verify the JWT yourself β€” the signing secret never leaves AIVerID, so introspection is the only verification path, which is also what makes a revoke take effect in seconds rather than at expiry.

Why so strict: the agent_name on an authorization request is supplied by the caller, so it is a label, not evidence. Anyone who can drive your client's OAuth flow can leave it out and get an ordinary web token carrying the same scopes. The only fields that are evidence are the ones AIVerID stamps and you cannot forge β€” token_use, grant_id, and aud.

Register your resource identifier first

A client that has not registered a resource cannot open an agent flow at all β€” the authorization request is refused rather than producing a token no one is allowed to accept. Send us the identifier you want and we add it to your client.

RFC 8707 requires the value to be an absolute URI and forbids a fragment component. The rest of what we enforce is stricter than the RFC, and we would rather you knew which is which than mistake our policy for the standard:

Keep version numbers in your endpoint paths, not in the identifier, so v1 costs you nothing later.

Once registered, resource becomes mandatory on your agent authorization requests and must match the registered value exactly; sending a different one, or none, is rejected with invalid_target. Your ordinary web sign-ins are unaffected and continue to receive tokens with no aud.

Introspect on every call

POST https://aiverid-backend-production.up.railway.app/oauth/introspect
Content-Type: application/x-www-form-urlencoded

token=<the bearer token>&client_id=<yours>&client_secret=<yours>

// active response β€” agent fields appear only for agent tokens
{
  "active": true,
  "sub": "TH...",
  "client_id": "aiv_yourapp_...",
  "scope": "yourapp:read yourapp:propose",
  "exp": 1787136103,
  "iat": 1787132503,
  "token_type": "access_token",
  "username": "TH...",
  "verification_level": 1,
  "verification_badge": "basic",
  "aud": "https://yourapp.xyz/api/agent",
  "token_use": "agent",
  "grant_id": "agt_0123456789abcdef0123456789abcdef",
  "act": { "sub": "agt_0123...", "name": "Claude Code on Job's laptop" }
}

Introspect with the same client_id the token was issued to β€” AIVerID answers active: false for another client's token, so one service can never probe another's sessions. Cache the result for at most 60 seconds: longer and a revoked agent keeps working, and your users' "last used" column goes stale, because introspection is what marks an agent as active.

Publish your resource metadata

Host this yourself so an agent can discover where to authenticate without being told (RFC 9728). The path of your resource identifier is inserted after the well-known suffix β€” an identifier of https://yourapp.xyz/api/agent is published at /.well-known/oauth-protected-resource/api/agent, not at the bare well-known path. An identifier with no path uses the bare path.

GET https://yourapp.xyz/.well-known/oauth-protected-resource/api/agent
Content-Type: application/json
Cache-Control: public, max-age=3600
Access-Control-Allow-Origin: *

{
  "resource": "https://yourapp.xyz/api/agent",
  "authorization_servers": [
    "https://aiverid-backend-production.up.railway.app"
  ],
  "scopes_supported": ["yourapp:read", "yourapp:validate", "yourapp:propose"],
  "bearer_methods_supported": ["header"],
  "resource_name": "YourApp Agent API",
  "resource_documentation": "https://yourapp.xyz/docs/agent"
}

resource is the only required member, and its value must be byte-identical to what you registered with us. Do not publish jwks_uri: AIVerID signs with HS256 and there are no public keys to fetch β€” a JWKS pointer would invite exactly the offline verification that makes revocation stop working.

Refuse in a way the agent can act on

// expired, revoked, wrong audience, or not an agent token β†’ 401
WWW-Authenticate: Bearer realm="yourapp.xyz",
  error="invalid_token",
  error_description="The access token expired",
  resource_metadata="https://yourapp.xyz/.well-known/oauth-protected-resource/api/agent"

// authenticated, but the grant lacks the scope β†’ 403
WWW-Authenticate: Bearer realm="yourapp.xyz",
  error="insufficient_scope",
  scope="yourapp:propose",
  resource_metadata="https://yourapp.xyz/.well-known/oauth-protected-resource/api/agent"

The resource_metadata pointer is what lets a well-built agent recover on its own instead of waiting for a human to reconfigure it. Two details from RFC 6750: a request carrying no credentials at all should get the bare challenge with no error code, and a wrong audience is invalid_token β€” say that it failed, never which of the four checks it failed, or you have written an oracle.

Before you open the surface

Letting claude.ai or ChatGPT connect to your MCP server

If you expose your agent API as a remote MCP server, a user can add it to claude.ai, Claude Desktop or ChatGPT as a custom connector by pasting its URL. In that flow the host is the OAuth client, not your service. AIVerID does not offer dynamic client registration (RFC 7591) β€” an anonymous registrant would choose its own name on the consent screen, which is a phishing kit on the one screen every user trusts. Instead each host has a pre-registered public client, which is the alternative the MCP authorization specification itself describes:

client_id   aiv_claude     redirect  https://claude.ai/api/mcp/auth_callback
client_id   aiv_chatgpt    redirect  https://chatgpt.com/connector_platform_oauth_redirect

client_secret   none β€” public client, PKCE S256 required
resource        the canonical URI of your MCP server, exactly as your
                protected-resource metadata publishes it

The user enters the client id in the host's advanced OAuth settings and leaves the secret blank. There is no secret on purpose: an id that every user of a connector pastes cannot keep a companion secret, and a secret that is public knowledge is worse than none. Possession is proven by PKCE, which both hosts implement.

Every authorization from these clients is an agent flow. The host never names itself, so AIVerID supplies the name β€” the grant appears as Claude or ChatGPT under the user's connected agents with its own revoke button, the token carries token_use: "agent", a grant_id and an aud equal to your MCP server URI, and the four checks above apply unchanged. The account scopes tokens and business are refused before consent, exactly as for any agent.

To open your MCP server to these hosts, send us its canonical URI and the scopes it accepts; we add them to the host clients. One caution on the identifier: MCP hosts send the URL the user pasted as the resource, so publish that exact value in your protected-resource metadata and compare aud against it at the MCP surface. If your MCP handler calls an inner API of your own, mint or map credentials there β€” never forward the host's token, whose audience is the MCP server and nothing else.

Server Metadata

Endpoints, scopes and supported flows are published as OAuth 2.0 Authorization Server Metadata (RFC 8414), so your client library can discover them instead of hard-coding URLs. AIVerID is OAuth 2.0 with PKCE β€” not OpenID Connect: there is no id_token and no JWKS, and identity claims come from the userinfo endpoint.

GET https://aiverid-backend-production.up.railway.app/.well-known/oauth-authorization-server

{
  "issuer": "https://aiverid-backend-production.up.railway.app",
  "authorization_endpoint": ".../oauth/authorize",
  "token_endpoint": ".../oauth/token",
  "userinfo_endpoint": ".../oauth/userinfo",
  "introspection_endpoint": ".../oauth/introspect",
  "revocation_endpoint": ".../oauth/revoke",
  "scopes_supported": ["business", "email", "profile", "tokens", ...],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["client_secret_post", "none"],
  "revocation_endpoint_auth_methods_supported": ["client_secret_post", "none"],
  "openid_connect_supported": false
}

Read this document rather than copying values from here β€” scopes_supported grows as applications register resource scopes, and requesting a scope your client is not registered for is rejected with invalid_scope.

Get Started with AIVerID OAuth

Register your application to get OAuth credentials

1
Contact Our Team
Send us your application details and use case
2
Get Credentials
Receive your client_id and client_secret
3
Test Integration
Use our OAuth Console to test your implementation
4
Go Live
Launch with confidence and our support

Ready to integrate?

Email us at developers@aiverid.com