Skip to main content
User token authentication lets your application act on behalf of a specific user. It connects to a dedicated OIDC application (such as Nail It or another VSP), and the resulting token is scoped to that user’s permissions only. This flow has two parts: token exchange for API access, and widget tokens for embedded UI components.

Token exchange for user impersonation

Tesouro supports OAuth 2.0 Token Exchange (RFC 8693). Your application exchanges a user JWT from your OIDC provider for a Tesouro access token scoped to that user.
Request
curl --location 'https://api.sandbox.tesouro.com/openid/connect/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
  --data-urlencode 'client_id=CLIENT_ID' \
  --data-urlencode 'client_secret=CLIENT_SECRET' \
  --data-urlencode 'subject_token=USER_JWT_TOKEN' \
  --data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:access_token'
Response
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 3600
}
Your JWT must include the user’s unique identifier in the sub claim and their email in the email claim.
Users can only be impersonated within their assigned application context. Cross-application impersonation is prevented.

Widget authentication

For embedded widgets, your backend generates a widget token — an encrypted JWE (RFC 7516) that securely wraps the user’s identity and OAuth credentials. This token is passed to the widget on the frontend. Your backend must provide:
  • The user’s identity (ID and email)
  • Your OAuth client credentials
  • A token expiration that matches your security requirements
Widget token generation
import { EncryptJWT } from "jose";

async function createWidgetToken(userId: string, userEmail: string) {
  const clientId = process.env.TESOURO_CLIENT_ID;
  const clientSecret = process.env.TESOURO_CLIENT_SECRET;
  const sharedSecret = process.env.TESOURO_WIDGET_SECRET; // 32 bytes

  const now = Math.floor(Date.now() / 1000);
  const secretKey = new TextEncoder().encode(sharedSecret);

  // Step 1: Create subject token (user identity)
  const subjectTokenPayload = {
    sub: userId,
    email: userEmail,
    aud: "tesouro",
    iss: clientId,
    exp: now + 300,
    iat: now,
  };

  const subjectToken =
    Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url") +
    "." +
    Buffer.from(JSON.stringify(subjectTokenPayload)).toString("base64url") +
    ".";

  // Step 2: Create encrypted widget token
  const payload = {
    aud: "tesouro",
    client_id: clientId,
    client_secret: clientSecret,
    exp: now + 300,
    grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
    iat: now,
    iss: clientId,
    jti: crypto.randomUUID(),
    subject_token: subjectToken,
    subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
  };

  const token = await new EncryptJWT(payload)
    .setProtectedHeader({
      alg: "A256KW",
      enc: "A256GCM",
      kid: clientId,
      typ: "JWT",
      cty: "JWT",
    })
    .encrypt(secretKey);

  return token;
}
Tesouro provides TESOURO_CLIENT_ID, TESOURO_CLIENT_SECRET, and TESOURO_WIDGET_SECRET (exactly 32 bytes) during onboarding.