Authentication
The frontend uses the same Better Auth system as the CMS. There is no second auth service, and a website account has role none: no CMS access. See Authentication concepts.
Login, register and account screens are ordinary pages in your Astro project. The CMS never renders them. The new-site starter includes a complete set under src/pages/account/.
Call the mutating methods from the browser through the same-origin proxy:
import { browserCms, KenresoftApiError } from '../lib/kenresoft';
try { const result = await browserCms.auth.signIn({ email, password }); if (result.twoFactorRequired) { await browserCms.auth.twoFactor.verifyTotp({ code }); } window.location.href = '/account';} catch (err) { if (err instanceof KenresoftApiError && err.code === 'EMAIL_NOT_VERIFIED') { // a fresh verification link has already been sent }}For a server-rendered “is this visitor signed in?”, forward the cookie:
const session = await cmsForRequest(Astro.request).auth.getSession(); // null when signed outMethods
Section titled “Methods”| Method | Endpoint | Notes |
|---|---|---|
signUp({ email, password, name, callbackUrl?, turnstileToken? }) |
POST /api/v1/auth/sign-up/email |
Emails a verification link. Resolves { requiresEmailVerification: true } with no session |
signIn({ email, password, rememberMe?, callbackUrl? }) |
POST /api/v1/auth/sign-in/email |
Resolves { twoFactorRequired, user }. Throws 401 INVALID_EMAIL_OR_PASSWORD or 403 EMAIL_NOT_VERIFIED (a fresh link is sent) |
signOut() |
POST /api/v1/auth/sign-out |
Idempotent |
getSession() |
GET /api/v1/auth/get-session |
{ user, session }, or null when signed out |
verifyEmail({ token }) |
GET /api/v1/auth/verify-email |
For the page your callbackUrl points at |
resendVerificationEmail({ email, callbackUrl? }) |
POST /api/v1/auth/send-verification-email |
Always returns the same generic message |
requestPasswordReset({ email, redirectUrl? }) |
POST /api/v1/public/password-reset/request |
Always the same generic message. The emailed link becomes <redirectUrl>?token=… |
resetPassword({ token, newPassword }) |
POST /api/v1/public/password-reset/confirm |
400 for an invalid or expired token |
changePassword({ currentPassword, newPassword, revokeOtherSessions? }) |
POST /api/v1/auth/change-password |
Needs a session. Signs out other devices by default |
twoFactor.enable, verifyTotp, verifyBackupCode, disable, generateBackupCodes |
/api/v1/auth/two-factor/* |
TOTP and backup codes only |
callbackUrl and redirectUrl must be origins listed in the CMS CORS_ORIGINS. Password-reset and verification responses are deliberately generic so they never reveal whether an account exists.
Endpoints
Section titled “Endpoints”POST /api/v1/auth/sign-up/emailPOST /api/v1/auth/sign-in/emailPOST /api/v1/auth/sign-outGET /api/v1/auth/get-sessionGET /api/v1/auth/verify-emailPOST /api/v1/auth/send-verification-emailPOST /api/v1/auth/change-passwordPOST /api/v1/auth/two-factor/{enable,verify-totp,verify-backup-code,disable,generate-backup-codes}POST /api/v1/public/password-reset/requestPOST /api/v1/public/password-reset/confirmLimits and protections
Section titled “Limits and protections”- Auth
POSTroutes are limited to 10 per 60 seconds per client IP. Password reset is limited to 3. See Rate limiting. - Sign-in requires a verified email, so the CMS needs a working email provider before launch.
- Optional bot protection on sign-up: create a Turnstile widget at dash.cloudflare.com → Turnstile, set it up on the API with
pnpm run update -- --turnstile(which stores the secret key and the site key), and passturnstileTokentosignUp. When set, the API returns400 TURNSTILE_REQUIREDwithout a token,403 TURNSTILE_FAILEDfor a bad or reused one, and503if Cloudflare cannot be reached. Tokens are single use. Your frontend needs the site (public) key to show the widget: read it from the API withsystem.status()(turnstileSiteKey,nullwhen not set), or setPUBLIC_TURNSTILE_SITE_KEYin the site yourself; see Environment variables.
Commerce customers
Section titled “Commerce customers”cms.commerce.customerAuth and commerce.customer.changePassword() call this same auth API, so a session created either way is the same session the cart, checkout and account routes read. See Commerce.