Complete authentication flows including email/password, social login, session management, and refresh tokens
Zooly Auth provides multiple authentication flows for different use cases:
returnTo parameterAll flows result in the same session cookies shared across all *.zooly.ai subdomains.
The standard authentication flow uses redirects with a returnTo parameter to bring users back to their original destination after login.
Each app redirects unauthenticated users to:
https://auth.zooly.ai?returnTo=https://zooly.ai/my-app
To prevent open redirects, returnTo URLs are validated:
Server-side validation:
javascript: and data: URLsClient-side validation:
javascript: and data: URLsNote: For local development, validation is relaxed to allow localhost and relative paths.
After successful authentication:
.zooly.ai (both ID token and refresh token)returnTo URL1. User visits: https://app.zooly.ai/dashboard
2. App detects no session → Redirects to: https://auth.zooly.ai?returnTo=https://app.zooly.ai/dashboard
3. User logs in at auth.zooly.ai
4. Auth app sets cookies for .zooly.ai (auth-token and auth-refresh-token)
5. Auth app redirects to: https://app.zooly.ai/dashboard
6. User arrives back at dashboard with valid session
sequenceDiagram
participant User
participant LoginPage
participant AuthAPI
participant Cognito
participant DynamoDB
User->>LoginPage: Enters email
LoginPage->>LoginPage: Shows SignUpForm
User->>LoginPage: Enters password + display name
LoginPage->>AuthAPI: POST /api/auth/signup
AuthAPI->>Cognito: signUp(email, password)
Cognito-->>AuthAPI: User created (unconfirmed)
AuthAPI->>DynamoDB: upsertIdentity (if new)
AuthAPI->>DynamoDB: linkCognitoIdentity (if guest exists)
Cognito->>User: Sends verification code via email
AuthAPI-->>LoginPage: Success
LoginPage->>LoginPage: Shows ConfirmSignUpForm
User->>LoginPage: Enters verification code
LoginPage->>AuthAPI: POST /api/auth/confirm
AuthAPI->>Cognito: confirmSignUp(email, code)
Cognito-->>AuthAPI: User confirmed
AuthAPI->>AuthAPI: signIn(email, password)
Cognito-->>AuthAPI: idToken + refreshToken
AuthAPI-->>LoginPage: Sets cookies + redirects to returnTo
Steps:
LoginFormSignUpFormPOST /api/auth/signup
linkCognitoIdentity)ConfirmSignUpFormPOST /api/auth/confirm
auth-token and auth-refresh-token)returnTosequenceDiagram
participant User
participant LoginPage
participant AuthAPI
participant Cognito
participant DynamoDB
User->>LoginPage: Enters email
LoginPage->>LoginPage: Shows PasswordForm
User->>LoginPage: Enters password
LoginPage->>AuthAPI: POST /api/auth/login
AuthAPI->>Cognito: signIn(email, password)
Cognito-->>AuthAPI: idToken + refreshToken
AuthAPI->>AuthAPI: verifyToken(idToken)
AuthAPI->>DynamoDB: getIdentity(sub)
AuthAPI->>DynamoDB: linkCognitoIdentity (if guest exists)
AuthAPI-->>LoginPage: Sets cookies + redirects to returnTo
Steps:
LoginFormPasswordFormPOST /api/auth/login
USER_PASSWORD_AUTH flow)linkCognitoIdentity)auth-token and auth-refresh-token)returnTosequenceDiagram
participant User
participant LoginPage
participant AuthAPI
participant Cognito
User->>LoginPage: Clicks "Forgot password"
LoginPage->>LoginPage: Shows ForgotPasswordForm
User->>LoginPage: Enters email
LoginPage->>AuthAPI: POST /api/auth/forgot-password
AuthAPI->>Cognito: forgotPassword(email)
Cognito->>User: Sends reset code via email
AuthAPI-->>LoginPage: Success
LoginPage->>LoginPage: Shows ResetPasswordForm
User->>LoginPage: Enters code + new password
LoginPage->>AuthAPI: POST /api/auth/reset-password
AuthAPI->>Cognito: confirmForgotPassword(email, code, newPassword)
Cognito-->>AuthAPI: Password reset successful
AuthAPI-->>LoginPage: Success - user can now login
Steps:
ForgotPasswordFormPOST /api/auth/forgot-password
ResetPasswordFormPOST /api/auth/reset-password
sequenceDiagram
participant User
participant AuthApp
participant Cognito
participant Google/Apple
participant AuthAPI
User->>AuthApp: Clicks "Continue with Google"
AuthApp->>AuthAPI: GET /api/auth/social/google?returnTo=...
AuthAPI->>Cognito: Generate hosted UI URL
AuthAPI-->>AuthApp: Redirect to Cognito hosted UI
AuthApp->>Cognito: User authenticates with Google
Cognito->>Google/Apple: OAuth flow
Google/Apple-->>Cognito: Authorization code
Cognito-->>AuthApp: Redirect to /api/auth/callback?code=...
AuthApp->>AuthAPI: GET /api/auth/callback?code=...
AuthAPI->>Cognito: exchangeCodeForTokens(code)
Cognito-->>AuthAPI: idToken + refreshToken
AuthAPI->>AuthAPI: verifyToken(idToken)
AuthAPI->>DynamoDB: upsertIdentity / linkCognitoIdentity
AuthAPI-->>AuthApp: Sets cookies + redirects to returnTo
Steps:
GET /api/auth/social/[provider]?returnTo=...
returnTo in cookie for callback/api/auth/callback?code=...GET /api/auth/callback
exchangeCodeForTokens)auth-token and auth-refresh-token)returnToNote: Social login is currently implemented and working. The flow uses the same session cookies as email/password authentication.
When a user registers or logs in with an email that was previously used for a guest checkout:
sequenceDiagram
participant User
participant AuthAPI
participant Cognito
participant DynamoDB
User->>AuthAPI: Login/Signup with email
AuthAPI->>Cognito: Authenticate
Cognito-->>AuthAPI: idToken (contains email)
AuthAPI->>DynamoDB: findIdentityByEmail(email)
DynamoDB-->>AuthAPI: Guest identity found
AuthAPI->>DynamoDB: linkCognitoIdentity(user_id, cognito_sub)
Note over DynamoDB: Sets cognito_sub, preserves user_id
AuthAPI-->>User: Session created (linked identity)
Process:
guest_emailuser_id using linkCognitoIdentitycognito_sub on existing identityuser_id)This enables:
Sessions are created when:
POST /api/auth/login)POST /api/auth/confirm)GET /api/auth/callback)Two cookies are set for each session:
auth-token (ID Token)
.zooly.ai (shared across all subdomains)true (not accessible to JavaScript)true (HTTPS only in production)Laxauth-refresh-token (Refresh Token)
.zooly.ai (shared across all subdomains)true (not accessible to JavaScript)true (HTTPS only in production)LaxImportant: Both cookies are HttpOnly and only accessible server-side. The refresh token is never exposed to client-side JavaScript.
When the ID token expires (after 24 hours), the system automatically refreshes it using the refresh token:
sequenceDiagram
participant Browser
participant AuthAPI
participant Cognito
Browser->>AuthAPI: GET /api/me (with cookies)
AuthAPI->>AuthAPI: verifyToken(idToken)
Note over AuthAPI: Token expired
AuthAPI->>AuthAPI: Read auth-refresh-token cookie
AuthAPI->>Cognito: REFRESH_TOKEN_AUTH(refreshToken)
Cognito-->>AuthAPI: new idToken + accessToken
AuthAPI->>AuthAPI: verifyToken(newIdToken)
AuthAPI-->>Browser: User profile + Set-Cookie: auth-token=newIdToken
Note over Browser: Cookie updated automatically
How it works:
/api/me (or any protected endpoint)auth-refresh-token cookieREFRESH_TOKEN_AUTH flowauth-token cookie in responseKey points:
Apps verify sessions by:
auth-token cookiesub, email) from token claimsJWT Verification:
aws-jwt-verify library with JWKSiss), audience (aud), expiration (exp)The verifyOrRefreshToken() helper function implements the refresh flow:
REFRESH_TOKEN_AUTH flowauth-token cookie with the new ID token in the responseThis ensures:
Mini-apps need a fast, in-page login experience without sending the user away from the app. We support this with a hybrid approach, using the same Cognito setup and the same session as the platform.
Mini-apps use exactly the same identity system:
Domain=.zooly.ai)From the backend's point of view, a user logged in from a mini-app is indistinguishable from one logged in from the platform.
For email/password authentication:
.zooly.aiThis gives the "quick login" experience without redirects.
For Google / Apple:
From the user's perspective:
From the system's perspective:
After login from a mini-app:
*.zooly.ai appLogout clears both session cookies:
POST /api/auth/logout
This removes both auth-token and auth-refresh-token cookies from the current browser. The cookies are cleared by setting them to expire immediately (Max-Age=0).
Note: This removes the session from the current browser but does not invalidate tokens server-side. Tokens remain valid until they expire naturally (24 hours for ID tokens, 90 days for refresh tokens).
Global sign-out (invalidating tokens server-side via Cognito) is not required for the current security posture but can be implemented if needed.
The refresh token implementation includes test endpoints for validation:
/api/test/refresh-flowReads both cookies and attempts to verify/refresh the ID token:
fetch('/api/test/refresh-flow', { credentials: 'include' })
.then(r => r.json())
.then(console.log)
Returns:
idTokenPresent, refreshTokenPresent)valid, expired, invalid)refreshAttempted, refreshSucceeded, newTokenIssued)/api/test/force-refreshForces a refresh token flow by directly calling refreshTokens():
fetch('/api/test/force-refresh', { credentials: 'include' })
.then(r => r.json())
.then(console.log)
Validates:
REFRESH_TOKEN_AUTH flow)Note: To test automatic refresh without waiting 24 hours, temporarily reduce Cognito ID token validity using AWS CLI (see setup documentation for details).
Users stay signed in for up to ~3 months without manual re-authentication.
auth-token cookie with Max-Age of 90 daysauth-refresh-token cookie with Max-Age of 90 days/api/me (or other protected endpoints) detects an expired ID token, it automatically uses the refresh token to get a new ID token and updates the cookieResult: Users stay logged in for the full 90-day refresh token lifetime without manual re-authentication, as long as they make requests at least once every 24 hours (ID token lifetime).
Identity data is split between two systems:
display_name, avatar_url, roles, guest_email)Important: Email is the source of truth in Cognito. It is read from the ID token and never stored in DynamoDB. This ensures email changes in Cognito are immediately reflected without stale data in DynamoDB.
The sub claim (Cognito user ID) serves as the identity anchor and primary key for DynamoDB lookups.
On This Page
OverviewRedirect + returnTo FlowEntry FlowreturnTo ValidationPost-Login FlowExample FlowEmail/Password Authentication FlowSign Up FlowLogin FlowForgot Password FlowSocial Login Flow (Google/Apple)OAuth FlowGuest User LinkingSession ManagementSession CreationSession CookiesAutomatic Token RefreshSession VerificationToken Verification and Refresh LogicMini-App Login SupportShared FoundationsPassword Login (Inline)Social Login (Seamless)Resulting BehaviorLogoutLocal LogoutGlobal Sign-OutTesting Refresh Token Flow[object Object][object Object]Long-Lived Sessions: 3 MonthsRequirementImplementationIdentity Storage