Skip to main content
The Connector lets one Ave app act on behalf of a user at another Ave-registered resource. A user approves the connection once; your app can then mint short-lived delegated JWTs whenever it needs to act on their behalf. A common use case: your app needs to access a partner service on a user’s behalf. The user consents via the Connector flow and your app exchanges tokens to get a scoped, time-limited JWT that the partner service accepts.

How it works

1

Start the connector consent flow

Redirect the user to https://aveid.net/connect with the target resource key, the scopes you want, and the communication mode.
const url = new URL("https://aveid.net/connect");
url.searchParams.set("client_id", "YOUR_CLIENT_ID");
url.searchParams.set("redirect_uri", "https://yourapp.com/callback");
url.searchParams.set("resource", "target-resource-key");
url.searchParams.set("scope", "resource.read");
url.searchParams.set("mode", "user_present");
url.searchParams.set("state", state);

window.location.href = url.toString();
resource is the resource key of the target Ave-registered resource (not a URL). scope must be one of the scopes defined by the target resource. mode is either user_present or background.
2

User approves the grant

The user sees the target resource’s information and what scopes your app is requesting. They choose which identity to use and approve. If they approve, Ave redirects back to your redirect_uri with a standard ?code=...&state=....
3

Exchange the authorization code

Exchange the code exactly like a regular auth code flow to get a source app access token:
import { exchangeCodeServer } from "@ave-id/sdk/server";

const sourceTokens = await exchangeCodeServer(
  {
    clientId: "YOUR_CLIENT_ID",
    clientSecret: process.env.AVE_CLIENT_SECRET!,
    redirectUri: "https://yourapp.com/callback",
  },
  { code }
);

// sourceTokens.access_token_jwt is what you use for the next step
4

Exchange for a delegated token

Use the token-exchange grant to mint a short-lived delegated JWT scoped to the target resource:
POST https://api.aveid.net/api/oauth/token
Content-Type: application/json

{
  "grantType": "urn:ietf:params:oauth:grant-type:token-exchange",
  "subjectToken": "ACCESS_TOKEN_JWT_FROM_PREVIOUS_STEP",
  "requestedResource": "target-resource-key",
  "requestedScope": "resource.read",
  "clientId": "YOUR_CLIENT_ID",
  "clientSecret": "YOUR_CLIENT_SECRET"
}
Use access_token_jwt as the subjectToken, not the opaque access_token. The server validates the JWT to resolve the user and client identity.
5

Use the delegated token at the target resource

The response contains a access_token that is a short-lived delegated JWT. Send it to the target resource API as a bearer token:
const response = await fetch("https://partner-service.example.com/api/data", {
  headers: {
    Authorization: `Bearer ${delegatedTokens.access_token}`,
  },
});
The delegated JWT expires in 10 minutes. Re-exchange when it expires (as long as the grant is still active).

Token exchange request

grantType
string
required
Must be exactly urn:ietf:params:oauth:grant-type:token-exchange.
subjectToken
string
required
The access_token_jwt from the source app’s token response. The server validates this JWT to identify the user and confirm the client ID.
requestedResource
string
required
The target resource key (as registered in the developer portal).
requestedScope
string
required
The scope(s) to include in the delegated token. Must be a subset of what was originally granted.
clientId
string
required
Your source app’s client ID.
clientSecret
string
required
Your source app’s client secret. This is a server-only call.

Token exchange response

access_token
string
A signed delegated JWT. Send this as the bearer token to the target resource.
token_type
string
Always Bearer.
expires_in
number
Token lifetime in seconds. Delegated tokens have a fixed 10-minute TTL.
scope
string
The actual scopes included in the delegated token.
audience
string
The audience of the target resource. The target resource should validate this.
target_resource
string
The target resource key.
communication_mode
string
Either user_present or background, as set when the grant was created.

Delegated JWT payload

The access_token from the exchange is a JWT with the following claims:
{
  "iss": "https://aveid.net",
  "sub": "identity-uuid",
  "aud": "target-resource-audience",
  "exp": 1712345678,
  "iat": 1712342078,
  "sid": "user-uuid",
  "cid": "source_app_client_id",
  "scope": "resource.read",
  "grant_id": "grant-uuid",
  "target_resource": "target-resource-key",
  "com_mode": "user_present"
}
The target resource should validate:
  • Signature against Ave’s JWKS
  • aud matches its registered audience
  • iss is https://aveid.net
  • exp > now
  • scope contains the required scope for the action
  • cid is a known/trusted source app (optional but recommended)

Server-side enforcement

Ave enforces these rules on every token exchange:
  • subjectToken must decode to a valid JWT belonging to the same clientId
  • The target resource must exist and be active
  • The delegation grant must exist and not be revoked
  • requestedScope must be a subset of the granted scope
  • requestedScope must be a known scope of the target resource

Grant management

Users can revoke grants. Your app should handle access_denied errors gracefully:
try {
  const delegatedTokens = await exchangeDelegatedTokenServer(config, {
    subjectToken: sourceAccessTokenJwt,
    requestedResource: "target-resource-key",
    requestedScope: "resource.read",
  });
} catch (err) {
  if (err.error === "access_denied") {
    // User revoked the grant — redirect them to re-connect
  }
}
You can also list and revoke grants via:
  • GET /api/oauth/delegations — list active grants (requires Ave session auth)
  • DELETE /api/oauth/delegations/:delegationId — revoke a grant

Error reference

ErrorCause
invalid_grantsubjectToken is invalid, expired, or for the wrong client
invalid_targetTarget resource key does not exist or is inactive
access_deniedNo active grant exists for this user/resource/source combination
invalid_scopeRequested scope exceeds granted scope or is not defined by the resource
Last modified on May 1, 2026