Callback
Learn how Wristband SDKs complete the login flow and return callback results.
After a user authenticates, Wristband redirects them to your Callback Endpoint with an authorization code and the state value from the login request. For example:
GET https://customer01.yourapp.io/auth/callback?state=f983yr893hf89ewn0idjw8e9f&code=shcsh90jf9wc09j9w0jewcThe SDK's callback function uses the Login State Cookie to verify the request, exchanges the authorization code for tokens, and retrieves the user's data from Wristband. It then returns a callback result, which tells your Callback Endpoint whether the login completed or needs to start over.
For example, using the Express Auth SDK:
app.get('/auth/callback', async (req, res) => {
const callbackResult = await wristbandAuth.callback(req, res);
const { callbackData, reason, redirectUrl, type } = callbackResult;
// Certain edge cases require a redirect back to the Login Endpoint.
if (type === 'redirect_required') {
console.debug(reason);
res.redirect(redirectUrl);
return;
}
// Create the application session from the callback data.
req.session.fromCallback(callbackData);
await req.session.save();
// Send the user back into your application.
res.redirect(callbackData.returnUrl || '<your_app_home_url>');
});How the Callback Works
When your Callback Endpoint calls the callback function, the SDK performs the following steps in order:
- Resolves the tenant: The SDK reads the tenant name from the request's subdomain when your
loginUrlcontains the{tenant_name}placeholder, or from thetenant_namequery parameter that Wristband adds otherwise (e.g.https://yourapp.io/auth/callback?state=f983yr893hf89ewn0idjw8e9f&code=shcsh90jf9wc09j9w0jewc&tenant_name=customer01). If Wristband also passes atenant_custom_domainquery parameter (e.g....&tenant_custom_domain=mytenant.com), the SDK validates it with the Validate Custom Domain API and ignores it if it isn't valid. - Verifies the Login State Cookie: The SDK finds the Login State Cookie that matches the
statequery parameter, clears it, decrypts it, and checks that itsstatevalue matches. - Checks for errors from Wristband: If Wristband passed an
errorquery parameter instead of an authorization code, the SDK handles it before going any further. - Exchanges the authorization code for tokens: The SDK calls the Wristband Token Endpoint with the authorization code, along with the PKCE code verifier and redirect URI stored in the Login State Cookie.
- Retrieves the user's data: The SDK calls the Wristband Userinfo Endpoint with the new access token. The data returned depends on the
scopesin your SDK configuration. - Validates the return URL: If the login request included a return URL, the SDK validates it before returning it.
See the Login State Cookie section of Login for more on the Login State Cookie.
Return Value
The callback function returns a callback result containing the following fields:
| Field | Type | Description |
|---|---|---|
type | string | The result type: completed or redirect_required. |
callbackData | CallbackData or undefined | The callback data used to create your application session. Only present for completed results. |
reason | string or undefined | Why the login couldn't be completed. Only present for redirect_required results. |
redirectUrl | string or undefined | The URL of your Login Endpoint to redirect the user to. Only present for redirect_required results, and only in SDKs that return a redirect URL. See Redirecting the User to Your App below. |
response | redirect response | The redirect response your Callback Endpoint returns. For completed results, it redirects to the URL from your resolver function. For redirect_required results, it redirects to your Login Endpoint. Only present in SDKs that return a redirect response. See Redirecting the User to Your App below. |
The callback function always returns one of two result types:
| Result Type | Description |
|---|---|
completed | The login succeeded, and the result includes the callback data needed to create your application session. |
redirect_required | The login can't be completed, and your Callback Endpoint should redirect the user back to your Login Endpoint to start a new login, without creating a session or doing any other processing. |
Redirecting the User to Your App
How your Callback Endpoint redirects the user depends on the SDK.
Responses Must Not Be CachedThe SDK sets the
Cache-Control: no-storeandPragma: no-cacheheaders on every response from your Callback Endpoint. These responses set or clear cookies and redirect to URLs that are only valid once, so a browser or intermediary cache must never reuse them.
Performing the Redirect Manually
Some SDKs leave the redirect to your Callback Endpoint (e.g. Express, and the Next.js Pages Router). For redirect_required results, the SDK provides your Login Endpoint's URL in redirectUrl. For completed results, your Callback Endpoint decides where to send the user.
For example, using the Express Auth SDK:
app.get('/auth/callback', async (req, res) => {
const callbackResult = await wristbandAuth.callback(req, res);
const { callbackData, reason, redirectUrl, type } = callbackResult;
// Certain edge cases require a redirect back to the Login Endpoint.
if (type === 'redirect_required') {
console.debug(reason);
res.redirect(redirectUrl);
return;
}
// Create the application session from the callback data.
req.session.fromCallback(callbackData);
await req.session.save();
// Send the user back into your application.
res.redirect(callbackData.returnUrl || '<your_app_home_url>');
});Returning the SDK’s Redirect Response
Some SDKs return a ready-made redirect response for both result types (e.g. the Next.js App Router). For redirect_required results, it redirects to your Login Endpoint, and your Callback Endpoint returns it as-is. For completed results, it redirects to the URL returned by a resolver function you provide, and your Callback Endpoint saves the session onto it before returning it.
For example, using the Next.js Auth SDK with the App Router:
export async function GET(req: NextRequest) {
const callbackResult = await wristbandAuth.appRouter.callback(req, (request, data) => {
return data.returnUrl || '/home';
});
const { callbackData, response, type } = callbackResult;
if (type === 'redirect_required') {
return response;
}
const session = await getRequestSession(req);
session.fromCallback(callbackData);
return await session.saveToResponse(response);
}The resolver is passed as the resolveRedirectUrl argument to the callback function, with the following signature, where Request is your framework's request type (e.g. NextRequest in Next.js):
resolveRedirectUrl: (request: Request, callbackData: CallbackData) => string | Promise<string>It receives the request and the callback data, and the SDK sets the URL it returns as the redirect location on the response:
- It only runs when the callback completes successfully.
- It can return the URL directly or as a Promise, such as after looking up data in your own database.
- The URL can be absolute or a relative path (e.g.
/home). A relative path is resolved against the host of the incoming request, which comes from theX-Forwarded-Hostheader whentrustProxyHeadersis enabled. - Returning an empty value throws a
TypeError.
For example, a resolver that sends new users to an onboarding page:
const callbackResult = await wristbandAuth.appRouter.callback(req, async (request, data) => {
const hasOnboarded = await db.hasCompletedOnboarding(data.userinfo.userId);
return hasOnboarded ? data.returnUrl || '/home' : '/onboarding';
});Completed
A completed result includes the callback data your Callback Endpoint uses to create the application session, typically by passing it to the session's fromCallback() function.
Creating the Session
Your Callback Endpoint creates the application session by passing the callback data to the session's fromCallback() function, as shown in the example at the top of this page. In some SDKs, fromCallback() only changes the session in memory, and your Callback Endpoint must still save the session so the SDK writes the session cookie. Others persist session changes automatically when the response is sent.
See the Session Fields section of Session Management for the fields fromCallback() stores.
Callback Data
| Callback Data Field | Type | Description |
|---|---|---|
accessToken | string | The access token for calling Wristband APIs and protecting your application's backend APIs. |
customState | object or undefined | The custom state your application provided during login, returned unmodified. |
expiresAt | number | The access token's expiration time, in milliseconds since the Unix epoch, with the tokenExpirationBuffer already subtracted. |
expiresIn | number | The number of seconds until the access token expires, with the tokenExpirationBuffer already subtracted. |
idToken | string | The ID token, which identifies the user and contains claims about them. |
refreshToken | string or undefined | The refresh token for getting new access tokens. Only present if the offline_access scope was requested and the Refresh Token grant type is enabled on your OAuth2 Client. |
returnUrl | string or undefined | The validated return URL from the login request. Not present if no return URL was provided or if it failed validation. |
tenantCustomDomain | string or undefined | The tenant custom domain used during login, if any. |
tenantName | string | The name of the tenant the user belongs to. |
userinfo | UserInfo | The user's data from the Wristband Userinfo Endpoint. |
UserInfo
The userinfo field maps the claims returned by the Wristband Userinfo Endpoint to the following fields. Which optional fields are present depends on the scopes in your SDK configuration.
| UserInfo Field | Type | Always Returned | Description |
|---|---|---|---|
userId | string | Yes | The ID of the user (from the sub claim). |
tenantId | string | Yes | The ID of the user's tenant (from the tnt_id claim). |
applicationId | string | Yes | The ID of the user's application (from the app_id claim). |
identityProviderName | string | Yes | The name of the identity provider the user logged in with (from the idp_name claim). |
fullName | string | No | The user's full name (from the name claim; requires the profile scope). |
givenName | string | No | The user's given or first name (requires the profile scope). |
familyName | string | No | The user's family or last name (requires the profile scope). |
middleName | string | No | The user's middle name (requires the profile scope). |
nickname | string | No | The user's casual name (requires the profile scope). |
displayName | string | No | The name the user prefers to be called (requires the profile scope). |
pictureUrl | string | No | The URL of the user's profile picture (requires the profile scope). |
email | string | No | The user's preferred email address (requires the email scope). |
emailVerified | boolean | No | Whether the user's email address has been verified (requires the email scope). |
gender | string | No | The user's gender (requires the profile scope). |
birthdate | string | No | The user's birthday in YYYY-MM-DD format (requires the profile scope). |
timeZone | string | No | The user's time zone (requires the profile scope). |
locale | string | No | The user's locale as a BCP47 language tag, e.g. en-US (requires the profile scope). |
phoneNumber | string | No | The user's phone number in E.164 format (requires the phone scope). |
phoneNumberVerified | boolean | No | Whether the user's phone number has been verified (requires the phone scope). |
updatedAt | number | No | When the user's information was last updated, as a Unix timestamp (requires the profile scope). |
roles | UserInfoRole[] | No | The roles assigned to the user (requires the roles scope). |
customClaims | object | No | Any custom claims configured for your application. |
Each role in the roles field has the following fields:
| UserInfoRole Field | Type | Description |
|---|---|---|
id | string | The globally unique ID of the role. |
name | string | The role name (e.g. app:app-name:admin). |
displayName | string | The human-readable display name of the role. |
Return URL Validation
Before returning the return URL from the login request, the SDK validates it to prevent open redirects, where a crafted login link could send a user to a site you don't control after a legitimate login. The SDK:
- Resolves relative URLs (e.g.
/settings) against the host of the callback request. - Keeps the URL only if it uses the same protocol as the callback request, and its host is either the exact host of the callback request or your application's root domain.
- If
applicationAuthorizationRequestsEnabledistrue, your application uses tenant subdomains, and the URL points at your root domain, adds the tenant's subdomain to it (e.g.https://yourapp.io/settingsbecomeshttps://customer01.yourapp.io/settings).
If the return URL fails validation or can't be parsed, the SDK drops returnUrl and excludes it from the callback data. Your Callback Endpoint should then redirect the user to a default page instead.
Redirect Required
A redirect_required result means the login can't be completed, and the user needs to start a new login.
Reasons
A redirect_required result includes a reason that explains why the login couldn't be completed:
| Reason | Description |
|---|---|
missing_login_state | The Login State Cookie wasn't found, such as when it expired or the user opened a bookmarked callback URL. |
invalid_login_state | The Login State Cookie couldn't be decrypted, or its state value didn't match the state query parameter. This can indicate a forged request or a tampered cookie. |
login_required | Wristband returned a login_required error, such as when the user's Wristband session expired. |
invalid_grant | The authorization code was invalid, expired, or already used. |
The redirect always points back to your Login Endpoint for the same tenant:
- With tenant subdomains: The tenant subdomain is used in the URL (e.g.
https://customer01.yourapp.io/auth/login). - Without tenant subdomains: The tenant is passed in the
tenant_namequery parameter (e.g.https://yourapp.io/auth/login?tenant_name=customer01). - With a tenant custom domain: It's also passed in the
tenant_custom_domainquery parameter (e.g.https://yourapp.io/auth/login?tenant_name=customer01&tenant_custom_domain=mytenant.com).
Errors From Wristband
If Wristband encounters an error while processing the login, it redirects to your Callback Endpoint with error and error_description query parameters instead of an authorization code. For example:
GET https://customer01.yourapp.io/auth/callback?state=f983yr893hf89ewn0idjw8e9f&error=login_required&error_description=User%20must%20re-authenticate%20because%20the%20specified%20max_age%20value%20has%20elapsedThe SDK handles a login_required error by returning a redirect_required result with the login_required reason.
For any other error, the SDK throws a WristbandError containing the error and its description. These errors typically come from SDK configuration issues during development and should be resolved before going to production.
Updated about 4 hours ago