User tokens
User token authentication is for browser and mobile apps where a real person signs in. TIE Auth is built on Google Identity Platform (GIP) — apps connect through TIE Auth to get shared user accounts, consistent token handling, and automatic user provisioning across all TIE services (memory, personas, agents).
Before You Start
Section titled “Before You Start”To integrate with TIE Auth, you need:
| What | Where to get it |
|---|---|
| TIE Auth base URL | Provided by the TIE team. Staging: https://tie-agent-service-auth-360927059581.us-central1.run.app |
| CORS whitelist | Request the TIE team to add your app's origin (e.g. http://localhost:3000, https://yourapp.com) |
| Google Client ID | From GCP Console (only if using Google OAuth sign-in) |
| Apple Client ID | From Apple Developer Console (only if using Apple sign-in) |
Registration (Email/Password)
Section titled “Registration (Email/Password)”Create a new user account. Password must be at least 6 characters.
POST https://your-auth-host/auth/registerContent-Type: application/json
{ "email": "user@example.com", "password": "SecurePassword123!", "display_name": "Jane Doe"}Response (201 Created):
{ "id_token": "eyJhbGciOiJSUzI1NiIs...", "refresh_token": "AMf-xB...", "expires_in": "3600", "user": { "id": "4b822c30-1a07-40d6-8d59-321ef99037f6", "gip_uid": "5iexdslxx7hFwmKPIdlssLRkREA3", "email": "user@example.com", "email_verified": false, "username": null, "display_name": "Jane Doe", "photo_url": null, "phone_number": null, "disabled": false, "role": "user", "metadata": {}, "created_at": "2026-03-25T10:00:00Z", "updated_at": "2026-03-25T10:00:00Z" }}All auth endpoints (/register, /login, /oauth/token) return this same response shape: an id_token for authenticating API calls, a refresh_token for getting new tokens when the current one expires, and a user object with the account details.
Error responses:
| Status | Code | When |
|---|---|---|
409 Conflict | AUTH_EMAIL_EXISTS | A user with this email is already registered |
400 Bad Request | AUTH_REGISTRATION_FAILED | Other registration failures (e.g. provider error) |
422 Unprocessable Entity | VALIDATION_ERROR | Invalid email format or password too short |
User IDs
Section titled “User IDs”user.id— TIE's canonical user UUID. Deterministic — the same Firebase user always produces the same UUID, across all services, without a central registry or database lookup.user.gip_uid— The raw Firebase/GIP UID. Also appears assubin the JWT.
Login (Email/Password)
Section titled “Login (Email/Password)”POST https://your-auth-host/auth/loginContent-Type: application/json
{ "email": "user@example.com", "password": "SecurePassword123!"}Returns the same { id_token, refresh_token, expires_in, user } shape as registration.
OAuth Sign-In (Google / Apple)
Section titled “OAuth Sign-In (Google / Apple)”For OAuth, the frontend handles the provider interaction (consent screen, token retrieval) and sends the resulting token to TIE Auth for verification. TIE Auth creates or links the user account automatically on first sign-in — no separate registration step needed.
sequenceDiagram
accTitle: OAuth Sign-In Flow
accDescr: Shows how a frontend app authenticates a user via Google or Apple through TIE Auth
participant User
participant Frontend
participant Provider as Google / Apple
participant TIE as TIE Auth
participant GIP as Google Identity Platform
User->>Frontend: Click "Sign in with Google/Apple"
Frontend->>Provider: Open consent screen
Provider-->>Frontend: id_token (JWT)
Frontend->>TIE: POST /auth/oauth/token { provider, id_token }
TIE->>GIP: Verify token
GIP-->>TIE: Verified user info
TIE->>TIE: Create or link user account
TIE-->>Frontend: { id_token, refresh_token, expires_in, user }
Frontend->>Frontend: Store tokens
Endpoint
Section titled “Endpoint”POST https://your-auth-host/auth/oauth/tokenContent-Type: application/json
{ "provider": "google", "id_token": "<token-from-provider-sdk>"}| Provider | provider value |
|---|---|
google | |
| Apple | apple |
Response shape is the same as registration.
Google Sign-In (Frontend — React)
Section titled “Google Sign-In (Frontend — React)”npm install @react-oauth/googleWrap your app root with the Google OAuth provider. The GOOGLE_CLIENT_ID is the OAuth 2.0 Client ID from your GCP project:
import { GoogleOAuthProvider } from "@react-oauth/google";
const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || "";
export function Providers({ children }: { children: React.ReactNode }) { return ( <GoogleOAuthProvider clientId={GOOGLE_CLIENT_ID}> {children} </GoogleOAuthProvider> );}Add the login button. Replace TIE_AUTH_BASE_URL with your TIE Auth service URL (e.g. from an environment variable):
import { GoogleLogin, type CredentialResponse } from "@react-oauth/google";
function LoginPage() { const handleGoogleSuccess = async (response: CredentialResponse) => { if (!response.credential) return;
const res = await fetch(`${TIE_AUTH_BASE_URL}/auth/oauth/token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider: "google", id_token: response.credential, }), }); const data = await res.json(); // data = { id_token, refresh_token, expires_in, user } localStorage.setItem("auth_token", data.id_token); localStorage.setItem("refresh_token", data.refresh_token); };
return ( <GoogleLogin onSuccess={handleGoogleSuccess} onError={() => console.error("Google sign-in failed")} size="large" theme="outline" /> );}Apple Sign-In (Frontend — React)
Section titled “Apple Sign-In (Frontend — React)”npm install react-apple-signin-authReplace APPLE_CLIENT_ID with the Service ID from your Apple Developer account, and TIE_AUTH_BASE_URL with your TIE Auth service URL:
import AppleSignin from "react-apple-signin-auth";
function LoginPage() { const handleAppleSuccess = async (response: any) => { const idToken = response.authorization?.id_token; if (!idToken) return;
const res = await fetch(`${TIE_AUTH_BASE_URL}/auth/oauth/token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider: "apple", id_token: idToken, }), }); const data = await res.json(); localStorage.setItem("auth_token", data.id_token); localStorage.setItem("refresh_token", data.refresh_token); };
return ( <AppleSignin authOptions={{ clientId: APPLE_CLIENT_ID, scope: "email name", redirectURI: window.location.origin, usePopup: true, }} onSuccess={handleAppleSuccess} /> );}Token Refresh
Section titled “Token Refresh”ID tokens expire after 1 hour. Use the refresh token to get a new one without re-authenticating:
POST https://your-auth-host/auth/refreshContent-Type: application/json
{ "refresh_token": "AMf-xB..."}Response:
{ "id_token": "eyJhbGciOiJSUzI1NiIs...", "refresh_token": "AMf-xB...", "expires_in": "3600"}Implement automatic refresh in your API client — when a request returns 401, refresh the token and retry:
const TIE_AUTH_BASE_URL = process.env.NEXT_PUBLIC_TIE_AUTH_BASE_URL || "";
async function fetchWithAuth(url: string, options: RequestInit = {}) { const token = localStorage.getItem("auth_token");
const response = await fetch(url, { ...options, headers: { ...options.headers, ...(token && { Authorization: `Bearer ${token}` }), }, });
if (response.status === 401) { const refreshToken = localStorage.getItem("refresh_token"); if (!refreshToken) throw new Error("Not authenticated");
const refreshRes = await fetch(`${TIE_AUTH_BASE_URL}/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: refreshToken }), });
if (!refreshRes.ok) throw new Error("Refresh failed");
const tokens = await refreshRes.json(); localStorage.setItem("auth_token", tokens.id_token); localStorage.setItem("refresh_token", tokens.refresh_token);
// Retry original request with new token return fetch(url, { ...options, headers: { ...options.headers, Authorization: `Bearer ${tokens.id_token}` }, }); }
return response;}Password Reset
Section titled “Password Reset”POST https://your-auth-host/auth/forgot-passwordContent-Type: application/json
{ "email": "user@example.com" }Always returns { "status": "ok" } regardless of whether the email exists (prevents email enumeration). Firebase sends a password reset email if the account exists.
Email Verification
Section titled “Email Verification”When a user registers with email/password, their email_verified field starts as false. TIE Auth automatically sends a verification email on registration — no extra API call needed.
The user object in all auth responses includes email_verified, so your app can check it and show a verification prompt when it's false.
Re-send verification email
Section titled “Re-send verification email”If the user did not receive the email or the link expired, they can request a new one:
POST https://your-auth-host/auth/send-verification-emailAuthorization: Bearer <id_token>Response (200 OK):
{ "status": "ok" }Returns 400 with code AUTH_VERIFICATION_FAILED if the email is already verified.
Checking verification status after the user clicks the link
Section titled “Checking verification status after the user clicks the link”When the user clicks the verification link, Google Identity Platform marks their email as verified on Google's side. Your app's token still carries the old email_verified: false until you refresh it.
The client-side flow:
sequenceDiagram
accTitle: Email Verification Refresh Flow
accDescr: Shows how a client detects email verification after the user clicks the link
participant User
participant App as Your App
participant TIE as TIE Auth
participant GIP as Google Identity Platform
App->>App: Show "Verify your email" banner
User->>User: Open email, click verification link
User->>GIP: Browser opens verification URL
GIP->>GIP: Mark email as verified
User->>App: Return to app
App->>TIE: POST /auth/refresh { refresh_token }
TIE-->>App: New id_token (email_verified: true)
App->>TIE: GET /auth/me
TIE-->>App: user.email_verified = true
App->>App: Hide verification banner
When to trigger the refresh — the client should call POST /auth/refresh when it detects the user has returned to the app after clicking the verification link:
Why OAuth users skip verification
Section titled “Why OAuth users skip verification”Users who sign in with Google or Apple already have a verified email — the provider confirms it during sign-in. Their email_verified is true from the first login, so no verification email is sent.
Using Tokens
Section titled “Using Tokens”Include the id_token as a Bearer token in all requests to TIE services. Note that the AI Gateway (your-tie-host) is a separate service from TIE Auth (your-auth-host):
curl -X POST https://your-tie-host/v1/chat/completions \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \ -H "Content-Type: application/json" \ -d '{"model": "anthropic/claude-sonnet-4-5", "messages": [{"role": "user", "content": "Hello"}]}'The token is a JWT signed by Firebase. TIE verifies the signature, checks expiry, and extracts the user identity.
Verifying Tokens in Your Backend
Section titled “Verifying Tokens in Your Backend”If your app has its own backend that needs to verify TIE tokens (e.g. protecting your own API routes), this section explains how.
TIE Auth issues Firebase ID tokens — JWTs signed by Firebase's service account keys with the GCP project ID as the audience. These are not standard Google OAuth2 tokens.
Token structure
Section titled “Token structure”{ "iss": "https://securetoken.google.com/<gcp-project-id>", "aud": "<gcp-project-id>", "sub": "5iexdslxx...", "tie_user_id": "4b822c30-...", "email": "user@example.com", "email_verified": false, "firebase": { "sign_in_provider": "password" }}Key claims:
aud— Always the GCP project ID (e.g.mv-stg-tie-core). This is Firebase's design, not configurable.sub— Firebase/GIP UID.tie_user_id— TIE's canonical user UUID (custom claim). Use this when calling TIE APIs. Set automatically on register, login, and OAuth sign-in.
Verification approaches
Section titled “Verification approaches”There are two valid approaches depending on your security requirements:
| Approach | Library | Signature verified | When to use |
|---|---|---|---|
| Full verification | firebase-admin | Yes | Public-facing APIs, production backends |
| Trusted decode | Manual JWT decode | No | Internal services that trust TIE over HTTPS |
User Profile
Section titled “User Profile”Get current user:
GET https://your-auth-host/auth/meAuthorization: Bearer <id_token>Returns the same user object from the login response.
Update profile:
PATCH https://your-auth-host/auth/meAuthorization: Bearer <id_token>Content-Type: application/json
{ "display_name": "Updated Name", "metadata": {"preferred_model": "anthropic/claude-sonnet-4-5"}}Metadata is shallow-merged: set a key to null to delete it. Max 50 keys, 16 KB total.
Usernames
Section titled “Usernames”Users can claim a unique username after registration. Usernames are unique within each tenant — two users in different tenants can have the same username, but no two users in the same tenant can.
Usernames are a one-time claim: once set, they cannot be changed.
| Rule | Value |
|---|---|
| Length | 3-30 characters |
| Allowed characters | Lowercase letters, numbers, hyphens (-), underscores (_) |
| Case | Auto-lowercased (e.g. JaneDoe becomes janedoe) |
| Reserved words | admin, system, support, root, me, user, login, register, signup, logout, settings, help, info, api, null, undefined, moderator, administrator, username |
Check availability
Section titled “Check availability”Check if a username is available before claiming. No authentication required.
GET https://your-auth-host/auth/username/available?username=janedoeWith tenant scoping (pass the internal tenant UUID, not the GIP tenant ID):
GET https://your-auth-host/auth/username/available?username=janedoe&tenant_id=<tenant-uuid>Response (200 OK):
{ "username": "janedoe", "available": true}Claim a username
Section titled “Claim a username”Claim a username for the authenticated user. This is a one-time operation — once claimed, it cannot be changed.
POST https://your-auth-host/auth/username/claimAuthorization: Bearer <id_token>Content-Type: application/json
{ "username": "janedoe"}Response (200 OK):
Returns the full user object with the username field set:
{ "id": "4b822c30-1a07-40d6-8d59-321ef99037f6", "username": "janedoe", "email": "jane@example.com", "display_name": "Jane Doe", ...}Error cases:
| Code | Status | When |
|---|---|---|
USER_USERNAME_TAKEN | 409 | Another user in the same tenant already claimed this username |
USER_USERNAME_ALREADY_CLAIMED | 409 | You already have a username (cannot change it) |
USER_USERNAME_INVALID | 422 | Username fails validation (too short, invalid characters, reserved word) |
Reading the username
Section titled “Reading the username”The username field is included in all user responses: GET /auth/me, login, registration, and OAuth sign-in responses. It is null until claimed.
Error Responses
Section titled “Error Responses”All errors follow a consistent format with a machine-readable code, human-readable message, optional details, and an actionable hint:
{ "code": "AUTH_TOKEN_INVALID", "message": "Invalid or expired token.", "details": null, "hint": "Refresh your access token and retry the request."}| Code | Status | When |
|---|---|---|
AUTH_TOKEN_MISSING | 401 | No Authorization header provided |
AUTH_TOKEN_INVALID | 401 | Token expired, invalid signature, or wrong audience |
AUTH_INVALID_CREDENTIALS | 401 | Wrong email/password at login |
AUTH_ACCOUNT_DISABLED | 403 | User account has been suspended |
AUTH_ADMIN_REQUIRED | 403 | Endpoint requires admin role |
AUTH_EMAIL_EXISTS | 409 | A user with this email already exists |
AUTH_REGISTRATION_FAILED | 400 | Registration failed due to invalid input |
AUTH_OAUTH_FAILED | 401 | OAuth provider token is invalid |
AUTH_OAUTH_PROVIDER_NOT_SUPPORTED | 400 | Provider is not google or apple |
AUTH_REFRESH_FAILED | 401 | Refresh token is invalid or expired |
AUTH_VERIFICATION_FAILED | 400 | Email is already verified, or verification email failed to send |
USER_USERNAME_TAKEN | 409 | Username already claimed by another user in the same tenant |
USER_USERNAME_ALREADY_CLAIMED | 409 | User already has a username (one-time claim) |
USER_USERNAME_INVALID | 422 | Username fails validation rules |
Endpoints Reference
Section titled “Endpoints Reference”| Endpoint | Method | Auth | Description |
|---|---|---|---|
/auth/register | POST | No | Register with email/password |
/auth/login | POST | No | Login with email/password |
/auth/oauth/token | POST | No | OAuth sign-in (Google/Apple) |
/auth/refresh | POST | No | Refresh an expired token |
/auth/forgot-password | POST | No | Send password reset email |
/auth/send-verification-email | POST | Yes | Re-send email verification link |
/auth/me | GET | Yes | Get current user profile |
/auth/me | PATCH | Yes | Update own profile |
/auth/username/available | GET | No | Check username availability |
/auth/username/claim | POST | Yes | Claim a username (one-time) |