Auth namespace
Reference for registration, login, token rotation, verification, password reset, magic links, and OAuth.
A
Jboard.auth creates and manages candidate or employer sessions for one board. It uses a short-lived bearer access token and an opaque, single-use refresh token. The SDK never navigates and never performs a hidden refresh after a 401—your application owns those decisions.
Session result
Registration, login, refresh, magic-link consumption, and OAuth exchange resolve a BoardAuthSession:
| Field | Type | Meaning |
|---|---|---|
object | 'board_auth_session' | Stable result discriminator |
accessToken | string | Bearer JWT, valid for one hour |
refreshToken | string | Rotating token, valid for 30 days until used or revoked. Reuse within 30 seconds returns the same rotated pair |
expiresAt | number | Access-token expiry in epoch milliseconds |
boardUser | BoardUser | The authenticated user on this board |
BoardUser fields:
| Field | Type | Meaning |
|---|---|---|
id | string | Board user ID |
object | 'board_user' | Stable resource discriminator |
role | candidate or employer | Which role this account is on the board |
email | string | Current email |
displayName | string or null | Display name |
emailVerified | boolean | Whether the current email is verified |
hasPassword | boolean | Whether the account has a password credential. false for magic-link and OAuth-only accounts. Use it to show change-password versus set-password. Set-password is board.auth.forgotPassword, not board.me.updatePassword. |
Storage and persistence
Methods that return a session also write its access and refresh tokens to the client’s configured auth.storage before resolving.
- Browser clients default to
memory; choosesessionorlocalexplicitly if the session must survive longer. - Server clients default to
nostore. Keep the returned pair in an application-owned httpOnly cookie and pass authorization per call. refreshreplaces both stored tokens with the rotated pair.logoutclears the access and refresh tokens only after server-side revocation succeeds.
See Authentication and session ownership before using a shared server client.
Registration and password login
| Method and signature | Returns | Behavior |
|---|---|---|
board.auth.register(body: RegisterBody, options?: FetchOptions) | Promise<BoardAuthSession> | Creates a candidate or employer using the emailpass method and persists the returned session. |
board.auth.login(body: LoginBody, options?: FetchOptions) | Promise<BoardAuthSession> | Authenticates an email/password pair and persists the returned session. |
RegisterBody requires role, method: 'emailpass', email, password, and displayName. Passwords must contain at least eight characters.
The SDK returns the session; it does not redirect to an account page or decide whether an unverified user can proceed.
Refresh and logout
| Method and signature | Returns | Behavior |
|---|---|---|
board.auth.refresh(body?: RefreshBody, options?: FetchOptions) | Promise<BoardAuthSession> | Rotates the refresh token, persists the replacement pair, and returns it. |
board.auth.logout(body?: LogoutBody, options?: FetchOptions) | Promise<void> | Revokes the refresh token and clears the configured access/refresh storage after the 204 response. |
When body is omitted, both methods read the refresh token from SDK storage. A nostore caller must pass it explicitly:
If neither the body nor storage supplies a refresh token, the SDK throws a plain Error before sending a request. It is not a BoardApiError because no API response exists.
Refresh tokens rotate on use. Presenting the same refresh token again within 30 seconds returns the same rotated pair, so concurrent refreshes converge; after that it returns 401. Coordinate concurrent refreshes with one shared in-flight promise or createSessionRefresher from @cavuno/board/server. Do not retry a refresh that returned 401.
Logout is idempotent on the server, including for an unknown refresh token. If the request fails because of a network or server error, the SDK deliberately retains local storage—the token might still be live server-side. Your application can retry revocation or explicitly choose a local-only sign-out.
Email verification
| Method and signature | Returns | Authentication |
|---|---|---|
board.auth.verifyEmail(body: VerifyEmailBody, options?: FetchOptions) | Promise<void> | The email-link token authorizes the request. |
board.auth.verifyEmailWithCode(body: { code: string }, options?: FetchOptions) | Promise<void> | Requires the signed-in user’s bearer token. |
board.auth.resendVerification(options?: FetchOptions) | Promise<void> | Requires the signed-in user’s bearer token. |
verifyEmail consumes the token included in the email link. verifyEmailWithCode consumes the six-digit code sent to the current user. Both resolve after a 204 response.
resendVerification sends a fresh code and link. It resolves as a no-op when the current user is already verified.
Employer work-email verification
| Method and signature | Returns | Authentication |
|---|---|---|
board.auth.verifyWorkEmail(body: ConfirmWorkEmailBody, options?: FetchOptions) | Promise<CompanyMembership> | The email-link token authorizes the request. |
When an employer claims a company, board.me.companies.workEmail.verify emails a verification link to their work address. verifyWorkEmail consumes the token from that link and promotes the pending claim: approved outright when the work-email domain matches the company, otherwise awaiting_admin.
No session and no company slug are required. The single-use token is both the authorization and the binding—it identifies the board user and the work email, which is all the server needs to find the pending claim. Every rejection (unknown, expired, already used, or issued for another board) is the same opaque 401; recover by sending a fresh verification email.
board.me.companies.workEmail.confirm(slug, body) is the deprecated predecessor. It still works and returns the same membership, but its slug argument is ignored by the server—prefer verifyWorkEmail.
Password reset
| Method and signature | Returns | Behavior |
|---|---|---|
board.auth.forgotPassword(body: ForgotPasswordBody, options?: FetchOptions) | Promise<void> | Accepts an email and always resolves 204 when the request is accepted, whether or not the account exists. |
board.auth.resetPassword(body: ResetPasswordBody, options?: FetchOptions) | Promise<void> | Consumes a single-use reset token, sets a password of at least eight characters, and invalidates existing sessions. |
The uniform forgot-password response prevents the endpoint from becoming an account-existence oracle.
After a successful reset, send the user through login again. Do not assume a previously stored access token remains valid.
Magic links
| Method and signature | Returns | Behavior |
|---|---|---|
board.auth.requestMagicLink(body: RequestMagicLinkBody, options?: FetchOptions) | Promise<void> | Sends a passwordless sign-in link and resolves 204. |
board.auth.consumeMagicLink(body: ConsumeMagicLinkBody, options?: FetchOptions) | Promise<BoardAuthSession> | Exchanges the email token once and persists the returned session. |
RequestMagicLinkBody accepts an email, an optional same-origin returnTo path, and an optional intent: 'sign_in'. Pass intent: 'sign_in' from a sign-in form so an unknown email is 404 board_auth_account_not_found instead of a silent sign-up.
The SDK does not navigate to returnTo; the host application owns the callback route and the post-exchange redirect.
OAuth
| Method and signature | Returns | Behavior |
|---|---|---|
board.auth.getOAuthAuthorizationUrl(provider: OAuthProvider, query?: OAuthAuthorizationQuery, options?: FetchOptions) | Promise<OAuthAuthorizationUrl> | Returns an authorization URL for 'google' or 'linkedin'. |
board.auth.exchangeOAuth(body: OAuthExchangeBody, options?: FetchOptions) | Promise<BoardAuthSession> | Exchanges the callback’s one-time token and persists the session. |
The URL method does not change browser location. Validate the callback route’s own redirect input instead of navigating to an arbitrary query parameter.
SSO connections
A board can add its own sign-in providers (for example a member system or a company identity provider). The operator switches every sign-in method on or off per role, so read what to offer from board.context() under signIn.candidate and signIn.employer instead of hard-coding buttons. Each role has this shape (BoardRoleSignIn):
Show a form or button for each method that is true and one button per entry in ssoConnections. A role with every built-in method false and at least one connection signs in only with SSO, so show only the SSO buttons. Everything false with no connections means that role cannot sign in on this board.
| Method and signature | Returns | Behavior |
|---|---|---|
board.auth.getSsoAuthorizationUrl(connectionId: string, query?: SsoAuthorizationQuery, options?: FetchOptions) | Promise<SsoAuthorizationUrl> | Returns the authorization URL for one SSO connection, for role 'candidate' (default) or 'employer'. |
board.auth.consumeSsoLinkProof(body: SsoLinkProofBody, options?: FetchOptions) | Promise<BoardAuthSession> | Completes a sign-in that had to confirm the email address first and persists the session. |
The sign-in lands on the same /auth/oauth-complete route as Google and LinkedIn. parseOAuthCompletion reads its query into one result:
token: exchange it withboard.auth.exchangeOAuth.link_proof_sent: the provider did not confirm the email of an existing account, so the user was emailed a confirmation link. Keep the browser secret established before authorization and tell the user to check their inbox;linkProofBindingin the URL is only a deprecated hash acknowledgment.link_proof: the user opened that link. Send it with the stored binding toboard.auth.consumeSsoLinkProof.error: the sign-in failed; the code is one ofSIGN_IN_REDIRECT_ERROR_CODES(failed sign-ins land on/auth/sign-in?error=with the same codes).
The initiating secret lives in localStorage, scoped to the board and kept for 25 minutes from the start of sign-in: up to ten minutes for provider authorization plus the proof email’s fifteen-minute validity after callback. This lets an emailed link work in a new tab of the same browser without extending the token’s expiry. If browser storage is unavailable, SDK initiation refuses before requesting authorization; server callers must provide their own independently retained browser proof. Opened anywhere else, consumeSsoLinkProof fails with board_auth_sso_browser_mismatch (isSsoBrowserMismatch): ask the user to open the link on the device where they started signing in.
To test SSO from a frontend on localhost or a preview deployment, pass developmentOrigin (for example window.location.origin) from a client that identifies the board with a publishable key (pk_...). The origin must be one of the board's development origins, and the sign-in completes there only while it is still registered. The confirmation email behind link_proof_sent is an email link, so it can only target a localhost development origin. On any other development origin, a sign-in that needs one lands on /auth/sign-in?error=sso_development_origin_not_allowed_for_email.
Any sign-in request for a method that is switched off for the role fails with 403 board_auth_method_unavailable: registration and login (password), requesting or consuming a magic link, Google and LinkedIn (authorization and exchangeOAuth), and consumeSsoLinkProof or exchangeOAuth for an SSO connection that is no longer switched on. isSignInMethodUnavailable narrows the error so error.details.availableMethods lists what the role can use instead: methods (built-in keys such as 'password' and 'magicLink') and ssoConnectionIds, which match signIn.<role>.ssoConnections.
A redirect-based sign-in refused the same way lands on /auth/sign-in?error=method_unavailable. getSsoAuthorizationUrl fails with board_auth_sso_connection_not_found for an unknown connection or one that is not in use, and with board_auth_sso_role_unavailable when the connection is not switched on for the requested role. After any of these, refresh board.context() and render the sign-in options again.
Fetch options and errors
Every method accepts FetchOptions in its final position. Standard fields such as signal and headers, plus framework fetch extensions, pass through to fetch.
Every non-2xx API response throws BoardApiError. Relevant codes include board_auth_account_not_found, board_auth_email_taken, board_auth_invalid_credentials, board_auth_invalid_token, board_auth_registration_disabled, board_auth_token_expired, board_auth_method_unavailable, and the board_auth_sso_* codes. Codes are additive within v1, so preserve a default error branch.
Use isUnauthorized for an expired or invalid session, but do not confuse it with isBoardPasswordRequired, which represents the separate board-password wall. Log error.requestId when present and never log either token.
Verify an auth implementation
- Confirm the selected storage receives both tokens after login and receives the rotated pair after refresh.
- Confirm two simultaneous expired-session requests produce one refresh attempt within the process.
- Confirm invalid and reused email, magic-link, reset, and OAuth tokens fail without being retried.
- Confirm logout clears local tokens only after revocation succeeds.
- On SSR, confirm access and refresh tokens never appear in HTML, serialized props, URLs, public caches, or browser JavaScript.
SSO browser proof
getSsoAuthorizationUrl establishes a random secret in this browser's board-scoped local storage before requesting authorization. Navigate to the returned URL: it first establishes an independent HttpOnly cookie on the central callback origin, then visits the provider. Cookies and local storage must be available. exchangeOAuth and consumeSsoLinkProof use the stored initiating secret automatically.
The completion URL's linkProofBinding is retained as a deprecated hash acknowledgment. Never save it as browser authority: ssoLinkProofBindingStore.save no longer installs or replaces a secret. A transferred callback or completion URL cannot complete sign-in in another browser.
Server or raw HTTP callers must establish and retain their own random browser secret before requesting authorization, pass its SHA-256 hex as browserBindingHash, and pass the original secret as browserBinding in the exchange or link-proof POST body. Missing proof fails closed. Hosted integrations start through /auth/sso/<connectionId> on their board; that route establishes the independent completion cookie.