Login
Learn how Wristband SDKs start the login flow and resolve the tenant domain.
The Login Endpoint is where every login starts. Your frontend redirects users to it whenever they need to log in. For example:
GET https://customer01.yourapp.io/auth/loginThe SDK's login function then determines which tenant the user is logging into, creates a Login State Cookie to track the login request, and builds the redirect that sends the user to Wristband to authenticate. Your Login Endpoint returns that redirect.
For example, using the Express Auth SDK:
app.get('/auth/login', async (req, res) => {
const loginUrl = await wristbandAuth.login(req, res);
res.redirect(loginUrl);
});The login function also accepts an optional login configuration for cases where your application needs custom behavior, such as setting a return URL or a default tenant. These options are covered in the sections below.
Tenant Resolution
Every login must be completed through a Tenant-Level domain by the time the user reaches your Callback Endpoint. To get there, the SDK first tries to resolve the tenant at your Login Endpoint, which determines the domain it uses when building the Wristband Authorize URL. It checks the following sources in precedence order, where each one resolves to a Tenant-Level domain:
tenant_custom_domainquery parameter: A tenant custom domain passed to your Login Endpoint (e.g.https://yourapp.io/auth/login?tenant_custom_domain=mytenant.com). The SDK first validates it with the Wristband Validate Custom Domain API, and if it isn't a valid tenant custom domain for your application, the SDK ignores it and moves on to the next source.- Tenant subdomain: The tenant name in the request's host, when your
loginUrlcontains the{tenant_name}placeholder (e.g.https://customer01.yourapp.io/auth/login). If your application runs behind a reverse proxy, see the Reverse Proxies section of Backend SDK Configuration for how the SDK determines the host. tenant_namequery parameter: A tenant name passed to your Login Endpoint, when yourloginUrldoesn't contain the placeholder (e.g.https://yourapp.io/auth/login?tenant_name=customer01).defaultTenantCustomDomainlogin configuration: A fallback tenant custom domain set by your application.defaultTenantNamelogin configuration: A fallback tenant name set by your application.
When No Tenant Is Resolved
If none of the above sources resolve a Tenant-Level domain, what happens next depends on your applicationAuthorizationRequestsEnabled configuration:
- When
true: The SDK builds the redirect to the Wristband Authorize Endpoint using your Application's vanity domain, and Wristband handles tenant discovery as part of the same login flow. - When
false: The SDK builds the redirect to the Application-Level Login (Tenant Discovery) Page instead, either the Wristband-hosted page or your self-hosted page ifcustomApplicationLoginPageUrlis set. After the user identifies their tenant, Wristband sends them back to your Login Endpoint to start a tenant-level login.
When a tenant can be resolved, such as when a user navigates directly to a Tenant-Level Login URL from a bookmark, the SDK always proceeds with a Tenant-Level login. This is especially relevant for Global and Standalone tenants, which are either excluded from tenant discovery entirely (Global Tenants) or intended to bypass Application-Level branding (Standalone Tenants). Users of these tenant types should always reach your Login Endpoint with their tenant already resolvable.
Default Tenants
If most or all of your users belong to the same tenant, you can set defaultTenantName or defaultTenantCustomDomain in the login configuration. Since a default always resolves a tenant, users skip tenant discovery entirely, and the SDK never falls back to the Application level.
For example, using the Express Auth SDK with a default tenant name:
const loginUrl = await wristbandAuth.login(req, res, { defaultTenantName: 'default' });
res.redirect(loginUrl);Or with a default tenant custom domain:
const loginUrl = await wristbandAuth.login(req, res, { defaultTenantCustomDomain: 'mytenant.com' });
res.redirect(loginUrl);Return Value
The login function returns where your Login Endpoint should redirect the user. Depending on how the SDK resolved the tenant, that destination is one of the following:
- The Wristband Authorize Endpoint on a Tenant-Level domain: When the SDK resolved a tenant.
- The Wristband Authorize Endpoint on your Application's vanity domain: When no tenant was resolved and
applicationAuthorizationRequestsEnabledistrue. - The Application-Level Login (Tenant Discovery) Page: When no tenant was resolved and
applicationAuthorizationRequestsEnabledisfalse. This is either the Wristband-hosted page or your self-hosted page ifcustomApplicationLoginPageUrlis set.
How the destination is returned depends on the SDK:
- As a URL: Some SDKs return the URL as a string, and your Login Endpoint performs the redirect itself (e.g. Express, and the Next.js Pages Router).
- As a redirect response: Some SDKs return a ready-made redirect response, and your Login Endpoint returns it as-is (e.g. the Next.js App Router).
Responses Must Not Be CachedThe SDK sets the
Cache-Control: no-storeandPragma: no-cacheheaders on every response from your Login 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.
Authorize URL Parameters
When the destination is the Wristband Authorize Endpoint, the SDK builds the URL with the following query parameters:
| Query Parameter | Value |
|---|---|
client_id | Your clientId. |
redirect_uri | Your redirectUri. |
response_type | Always code. |
state | The random state value stored in the Login State Cookie. |
scope | Your scopes, space-separated. |
code_challenge | The PKCE code challenge, derived from the code verifier stored in the Login State Cookie. |
code_challenge_method | Always S256. |
nonce | A random value generated for each login request. |
idp_hint | Forwarded from your Login Endpoint's idp_hint query parameter, if present. |
login_hint | Forwarded from your Login Endpoint's login_hint query parameter, if present. |
See the Authorize Endpoint documentation for more on each parameter.
Login Hint
The login_hint query parameter pre-fills the email field on the Tenant Login Page. Wristband passes it to your Login Endpoint in flows such as tenant discovery, after a user has already entered their email on the Application-Level Login Page. For example:
GET https://customer01.yourapp.io/auth/[email protected]IdP Hint
The idp_hint query parameter tells Wristband which identity provider the user should log in with. Its value is the name of an identity provider enabled for the tenant. For example:
GET https://customer01.yourapp.io/auth/login?idp_hint=googleWristband handles the hint as follows:
- External identity provider (e.g.
google): Wristband skips the Tenant Login Page and sends the user straight to that provider's login page. - Wristband identity provider: Wristband shows the Tenant Login Page with only the email-based login form.
- Unrecognized value: Wristband ignores the hint and shows the Tenant Login Page as normal.
Login State Cookie
Before redirecting the user to Wristband, the SDK creates a Login State Cookie that holds everything it needs to complete this specific login request at the Callback Endpoint. The SDK encrypts the cookie so its contents can't be read or modified.
The cookie contains the following:
| Field | Description |
|---|---|
state | A random value sent to Wristband with the authorization request and checked again at the callback. |
codeVerifier | The PKCE code verifier, used when exchanging the authorization code for tokens. |
redirectUri | The Callback Endpoint URL used for this login request. |
returnUrl | The return URL for this login, if one was provided. |
customState | Your application's custom state, if any was provided. |
When Wristband redirects the user back to your Callback Endpoint, the SDK finds the matching cookie, clears it, and checks that its state value matches the state query parameter. This ensures the callback belongs to a login request your application actually started, and protects against tampered or forged callback requests. If the cookie is missing, such as when it has expired or the user opened a bookmarked callback URL, or if the state values don't match, the callback returns a redirect to your Login Endpoint so a new login can start.
Concurrent Logins
Each Login State Cookie is named after its state value and creation time (e.g. login#{state}#{timestamp}), so logins started in multiple browser tabs don't overwrite each other. The SDK keeps up to three Login State Cookies at a time and clears the oldest when a new login starts. Each cookie expires after one hour.
Cookie Domain
The domain the Login State Cookie is set on depends on your configuration:
- When
applicationAuthorizationRequestsEnabledistrueand yourloginUrlcontains the{tenant_name}placeholder: The SDK sets the cookie on your application's root domain, which it derives by removing the placeholder from yourloginUrl(e.g.https://{tenant_name}.yourapp.io/auth/logingives.yourapp.io). This makes the cookie available across all tenant subdomains, which is needed because a login can start on your Application-Level domain before the tenant is known, while the callback arrives on the tenant's subdomain. - In all other cases: The SDK sets the cookie without a domain, so the browser only sends it back to the exact host that handled the login request (e.g.
customer01.yourapp.io).
Return URLs
Unauthenticated users often try to reach a specific page in your application, such as from a bookmark or a shared link, and get sent to log in first. A return URL lets the SDK carry that location through the login flow, so the user lands back where they started instead of on a default page.
You can provide a return URL in either of two ways:
returnUrllogin configuration: Set by your Login Endpoint. For example, using the Express Auth SDK:const loginUrl = await wristbandAuth.login(req, res, { returnUrl: 'https://customer01.yourapp.io/settings/profile', }); res.redirect(loginUrl);return_urlquery parameter: Passed to your Login Endpoint by your frontend. For example:GET https://customer01.yourapp.io/auth/login?return_url=https://customer01.yourapp.io/settings/profile
If both are present, the login configuration takes precedence.
The SDK stores the return URL in the Login State Cookie and returns it from the callback, where your Callback Endpoint can redirect the user to it. The SDK validates the return URL during the callback: relative URLs are resolved against the callback request, and URLs that don't match your application's domain are dropped.
Return URLs During Tenant Discovery
Application-Level Authorization RequestsThis section only applies when
applicationAuthorizationRequestsEnabledisfalse. When it'strue, the return URL is stored in the Login State Cookie as usual, and no extra steps are needed.
When applicationAuthorizationRequestsEnabled is false, the user goes to the Tenant Discovery Page before any Login State Cookie exists. The SDK appends the return URL to the Tenant Discovery Page URL (if provided) as the state query parameter, and Wristband includes that same state value when it redirects the user back to your Login Endpoint after tenant discovery. Your Login Endpoint must then pass it to the login function as returnUrl.
For example, the SDK sends the user to the Tenant Discovery Page with the return URL in state:
https://yourapp-yourcompany.us.wristband.dev/login?client_id=123&state=https%3A%2F%2Fcustomer01.yourapp.io%2FsettingsAfter tenant discovery, Wristband redirects the user back to your Login Endpoint with the same state value:
https://yourapp.io/auth/login?tenant_name=customer01&state=https%3A%2F%2Fcustomer01.yourapp.io%2FsettingsFor example, using the Express Auth SDK:
app.get('/auth/login', async (req, res) => {
const returnUrl = typeof req.query.state === 'string' ? req.query.state : undefined;
const loginUrl = await wristbandAuth.login(req, res, { returnUrl });
res.redirect(loginUrl);
});
State Parameter LengthThe
statequery parameter is limited to 512 characters. If the return URL is longer, it's dropped and the login continues without it.
Custom State
Custom state lets your application carry its own context through the login flow, such as which feature or action triggered the login. If you set customState in the login configuration, then the SDK stores it in the Login State Cookie and returns it unmodified from the callback. Empty customState objects are ignored.
For example, using the Express Auth SDK:
const loginUrl = await wristbandAuth.login(req, res, {
customState: { test: 'abc' }
});
res.redirect(loginUrl);
Use Custom State SparinglyMost applications don't need custom state. Browsers limit cookies to 4KB, so keep custom state small. Around 1KB of JSON is a safe ceiling.
Reference
Login Configuration
| Field | Type | Description |
|---|---|---|
customState | object | Application-specific state stored in the Login State Cookie and returned unmodified from the callback. |
defaultTenantCustomDomain | string | A fallback tenant custom domain used when no tenant can be resolved from the request. |
defaultTenantName | string | A fallback tenant name used when no tenant can be resolved from the request. |
returnUrl | string | The URL to send the user to after login. Takes precedence over the return_url query parameter. |
Login Endpoint Query Parameters
| Query Parameter | Description |
|---|---|
idp_hint | The name of an identity provider enabled for the tenant, used to send the user directly to that provider. |
login_hint | An email address used to pre-fill the Tenant Login Page. |
return_url | The URL to send the user to after login. |
state | The return URL passed back by Wristband after tenant discovery, when applicationAuthorizationRequestsEnabled is false. |
tenant_custom_domain | The tenant custom domain to log in through. |
tenant_name | The name of the tenant to log in to. Only used when your loginUrl doesn't contain the {tenant_name} placeholder. |
Updated about 5 hours ago