SSO-Only User Deprovisioning

Give your customers a way to immediately revoke a user's access to your application, without Wristband ever storing that user's data.

ℹ️

SSO-Only Integrations Only

This page covers deprovisioning users in SSO-only integrations, where your application owns and stores its own users. If your users are provisioned and stored in Wristband, refer to User Deprovisioning instead.

In an SSO-only integration, Wristband only brokers the login handshake. Your own application owns the session and access tokens afterward. Revoking access in the organization's IdP stops future logins, but doesn't affect access your application already granted. That employee can keep using the application until your session expires or you explicitly deprovision them.

As with the User Deprovisioning documentation for Wristband-stored users, you can build your own API layer (referred to throughout this page as a "wrapper API") that your customer calls directly to trigger deprovisioning themselves without needing to contact you.

The rest of this page covers how to implement the wrapper API.

⚠️

Confirm Requirements Before Building

Only consider building a wrapper API if your customer requires automated, self-service deprovisioning and that requirement isn't negotiable.


How the Wrapper API Works

Your customer calls a deprovision endpoint that you expose. Since your customer's system never interacts with Wristband directly, it won't have a Wristband userId on hand. It will only have whatever identifies the user on their own side, such as an externalId or email.

Your endpoint should:

  1. Authenticate the incoming customer request.
  2. Validate the target user's externalId or email from the request.
  3. Deactivate or delete that user directly in your own system.

That first step, authenticating the customer, is where Wristband comes in.

⚠️

Already Have Your Own API Keys?

If your application already has an API key system, you can reuse it instead, and skip 1) Create a Tenant-Level M2M Client for Your Customer and 2) Create a Token Endpoint Wrapper API entirely. Be aware that Wristband would not be involved, and authenticating incoming requests would be entirely your responsibility.

The rest of this page assumes you're using a Wristband tenant-level M2M client to authenticate your customer instead.

Here's how that looks end to end:

%%{init: {'themeVariables': {'fontSize': '16px'}}}%%
sequenceDiagram
    participant Customer as Customer's backend
    participant Wrapper as Your wrapper API
    participant Token as Wristband Token Endpoint
 
    Customer->>Wrapper: Token request (client creds)
    Wrapper->>Token: Proxy request
    Token-->>Wrapper: Access token
    Wrapper-->>Customer: Access token
 
    Customer->>Wrapper: Deprovision request (Bearer token)
    Wrapper->>Wrapper: Validate token
    Wrapper->>Wrapper: Deactivate or delete user
    Wrapper-->>Customer: 200 or 204

1) Create a Tenant-Level M2M Client for Your Customer

ℹ️

Important: Skip this section if you are using your own API key system.

To authenticate customer requests to your wrapper API, you can create a Wristband tenant-level M2M client just for them. They can use the clientId and clientSecret to obtain access tokens from Wristband and send those tokens with each request, which your wrapper API should validate before processing the request.

Refer to Tenant-Level Clients for instructions on creating a tenant-level M2M client. Since this client is only used to authenticate the customer's identity to your wrapper API, it doesn't need any Wristband resource permissions.

Once created, share the following information with your customer through a secure channel:

FieldDescription
Client IDThe clientId of the tenant-level M2M client you created for this customer.
Client SecretThe clientSecret of the tenant-level M2M client you created for this customer.

2) Create a Token Endpoint Wrapper API

ℹ️

Important: Skip this section if you are using your own API key system.

Since your customer never calls Wristband directly, they need somewhere to send their tenant-level M2M client's credentials to get access tokens. Expose a wrapper API that accepts those credentials, proxies the request to Wristband's Token Endpoint, and returns the access token to your customer.

Suggested Spec

Below is one suggested spec for this endpoint. The exact design is entirely up to you.

Request

POST /api/v1/auth/token HTTP/1.1
Host: yourapp.com
Content-Type: application/json
 
{
  "clientId": "abc123",
  "clientSecret": "def456"
}

Headers

NameRequiredDescription
Content-TypeYesMust be application/json.

Body

FieldTypeRequiredDescription
clientIdstringYesThe clientId of the tenant-level M2M client you issued to this customer.
clientSecretstringYesThe clientSecret of the tenant-level M2M client you issued to this customer.

Response

HTTP/1.1 200 OK
Content-Type: application/json
 
{
  "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expiresIn": 1800
}

HTTP Response Codes

StatusDescription
200 OKThe credentials were valid, and an access token is included in the response body.
400 Bad RequestThe request body was missing clientId or clientSecret.
401 UnauthorizedThe clientId and clientSecret combination was invalid.

Code Example

For example, a TypeScript function that proxies the request to Wristband's Token Endpoint:

async function handleTokenRequest(clientId: string, clientSecret: string) {
  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
 
  const response = await fetch(`https://${process.env.APPLICATION_VANITY_DOMAIN}/api/v1/oauth2/token`, {
    method: 'POST',
    headers: {
      Authorization: `Basic ${credentials}`,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: 'grant_type=client_credentials',
  });
 
  if (!response.ok) {
    throw new Error(`Failed to get token: ${response.status}`);
  }
 
  const { access_token, expires_in } = await response.json();
  return { accessToken: access_token, expiresIn: expires_in };
}

3) Create a Deprovision Endpoint Wrapper API

Since your users live entirely in your own system, this endpoint doesn't need to resolve a Wristband userId at all. It just needs enough information to identify the user in your own database: either an externalId matching what your customer's identity provider uses or the user's email.

Suggested Spec

Below is one suggested spec for this endpoint. The exact design is entirely up to you.

Request

POST /api/v1/users/deprovision HTTP/1.1
Host: yourapp.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
 
{
  "email": "[email protected]"
}

Headers

NameRequiredDescription
AuthorizationYesA Bearer token authenticating your customer's call. See Validate Your Customer's Requests.
Content-TypeYesMust be application/json.

Body

FieldTypeRequiredDescription
externalIdstringConditionallyWhatever identifier you use to look up the user in your own system. Required if email isn't provided.
emailstringConditionallyThe user's email address. Required if externalId isn't provided.
ℹ️

Note: Exactly one of externalId or email should be provided. If your wrapper API only supports one, drop the other from the spec entirely.

Response

HTTP/1.1 204 No Content

HTTP Response Codes

StatusDescription
204 No ContentThe user was successfully deprovisioned
400 Bad RequestThe request body was missing required fields, or included both externalId and email.
401 UnauthorizedThe Bearer token was missing, invalid, or failed tenant validation.
404 Not FoundNo user was found matching the provided identifier.

Validate Your Customer's Requests

In your deprovision endpoint, validate incoming access tokens using a Wristband JWT SDK. Since the deprovision endpoint itself doesn't include any customer or tenant identifier in the request, the token's tnt_id claim is how you determine who's calling it.

For reference, a decoded access token for a tenant-level M2M client looks like this:

{
  "sub": "sqi7mel4prbixj62rlruslyi64",
  "tnt_id": "ffioj5rc3bd6rnwr7jxhvycfoa",
  "van_dom": "invotastic-demo-wristband.us.wristband.dev",
  "iss": "https://invotastic-demo-wristband.us.wristband.dev",
  "sub_kind": "tenant_client",
  "exp": 1785197092,
  "app_id": "vl6jdobxy5gafkr3dvztbvzrue",
  "iat": 1785110692,
  "jti": "5hbgvgrlejglplvwokwttvazha",
  "client_id": "sqi7mel4prbixj62rlruslyi64"
}

Once the token's signature is verified, treat tnt_id as trusted, and use it to look up whatever you need to know about that customer.

Code Example

For example, using the TypeScript JWT SDK to validate the token and return the tenantId:

import { createWristbandJwtValidator } from '@wristband/typescript-jwt';
 
// Create once and reuse across requests.
const wristbandJwtValidator = createWristbandJwtValidator({
  wristbandApplicationVanityDomain: process.env.APPLICATION_VANITY_DOMAIN!,
});
 
async function validateCustomerRequest(authorizationHeader: string): Promise<string> {
  // Pull the raw Bearer token out of the Authorization header.
  const token = wristbandJwtValidator.extractBearerToken(authorizationHeader);
 
  // Verify the token's signature, issuer, and expiration.
  const result = await wristbandJwtValidator.validate(token);
 
  if (!result.isValid) {
    throw new Error(`Invalid token: ${result.errorMessage}`);
  }
 
  // A valid signature means this tnt_id can be trusted as the calling customer's tenant.
  const tenantId = result.payload?.tnt_id;
  if (!tenantId) {
    throw new Error('Token is missing a tenant ID');
  }
 
  return tenantId;
}

Mapping by Tenant Name

The token's tnt_id claim identifies the tenant by its Wristband ID. If your own system maps customers by the Wristband tenant name instead of tenant ID, you'll need to include the tenant's name in the access token.

The tenant's name isn't included in the access token by default. Add it as a custom claim from SecurityToken Settings in Application View using the tenant.name path expression and a claim name of your choosing (e.g., tnt_name).

Token Settings: Add a custom claim using the tenant.name path expression.

Token Settings: Add a custom claim using the tenant.name path expression.

Once configured, it appears nested under custom_claims in the token payload. For example:

{
  "sub": "sqi7mel4prbixj62rlruslyi64",
  "tnt_id": "ffioj5rc3bd6rnwr7jxhvycfoa",
  "van_dom": "invotastic-demo-wristband.us.wristband.dev",
  "iss": "https://invotastic-demo-wristband.us.wristband.dev",
  "sub_kind": "tenant_client",
  "exp": 1785197092,
  "app_id": "vl6jdobxy5gafkr3dvztbvzrue",
  "iat": 1785110692,
  "jti": "5hbgvgrlejglplvwokwttvazha",
  "client_id": "sqi7mel4prbixj62rlruslyi64",
  "custom_claims": {
    "tnt_name": "acme"
  }
}

Deprovision the User

Once the request is validated, look up the user in your own system using the externalId or email from the request, then deactivate or delete their record directly. This is entirely your own application logic, Wristband isn't involved at this step.

Your wrapper API doesn't necessarily need to support both deactivating and deleting. You can decide upfront to only ever do one, and build your deprovision endpoint around that single choice. If you do want to support both, your endpoint needs some way to distinguish which one a given request wants (e.g., a separate endpoint per action, an HTTP method per action, or a field in the request body).


Putting It All Together

Below is a single, complete TypeScript example of everything covered on this page: the token endpoint and the deprovision endpoint. It's written as plain functions, framework-agnostic, so you can wire them into whatever HTTP framework you're using.

📘

Additional Languages

For other languages and frameworks, refer to Authentication SDKs for the full list of available JWT validation SDKs.

import { createWristbandJwtValidator } from '@wristband/typescript-jwt';
 
const VANITY_DOMAIN = process.env.APPLICATION_VANITY_DOMAIN!;
 
// JWT validator used to validate the customer's incoming access tokens.
const wristbandJwtValidator = createWristbandJwtValidator({
  wristbandApplicationVanityDomain: VANITY_DOMAIN,
});
 
// Validates a customer's incoming request and returns the tenant it belongs to.
async function validateCustomerRequest(authorizationHeader: string): Promise<string> {
  const token = wristbandJwtValidator.extractBearerToken(authorizationHeader);
  const result = await wristbandJwtValidator.validate(token);
 
  if (!result.isValid) {
    throw new Error(`Invalid token: ${result.errorMessage}`);
  }
 
  const tenantId = result.payload?.tnt_id;
  if (!tenantId) {
    throw new Error('Token is missing a tenant ID');
  }
 
  return tenantId;
}
 
// Deprovisions a user identified by either externalId or email.
export async function deprovisionUser(
  authorizationHeader: string,
  externalId: string | undefined,
  email: string | undefined
) {
  if ((!externalId && !email) || (externalId && email)) {
    throw new Error('Provide exactly one of externalId or email');
  }
 
  const tenantId = await validateCustomerRequest(authorizationHeader);
 
  // NOTE: This is where you look up and deactivate or delete the user in
  // your own system. Scope the lookup to `tenantId` if your data model
  // supports it.
  await yourOwnUserStore.deactivateOrDelete({ tenantId, externalId, email });
}

(Optional) Client Secret Rotation

Since your customer holds a long-lived client secret for their tenant-level M2M client, you may want to let them rotate the secret on their own schedule without involving you directly.

This works the same way as it does for Wristband-stored users. Refer to Client Secret Rotation on the main User Deprovisioning page for the full spec and code example, since the mechanics are identical, only the deprovisioning logic itself differs between the two pages.




Did this page help you?