Skip to content

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).

To integrate with TIE Auth, you need:

WhatWhere to get it
TIE Auth base URLProvided by the TIE team. Staging: https://tie-agent-service-auth-360927059581.us-central1.run.app
CORS whitelistRequest the TIE team to add your app's origin (e.g. http://localhost:3000, https://yourapp.com)
Google Client IDFrom GCP Console (only if using Google OAuth sign-in)
Apple Client IDFrom Apple Developer Console (only if using Apple sign-in)

Create a new user account. Password must be at least 6 characters.

Terminal window
POST https://your-auth-host/auth/register
Content-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:

StatusCodeWhen
409 ConflictAUTH_EMAIL_EXISTSA user with this email is already registered
400 Bad RequestAUTH_REGISTRATION_FAILEDOther registration failures (e.g. provider error)
422 Unprocessable EntityVALIDATION_ERRORInvalid email format or password too short
  • 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 as sub in the JWT.
Terminal window
POST https://your-auth-host/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePassword123!"
}

Returns the same { id_token, refresh_token, expires_in, user } shape as registration.

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
Terminal window
POST https://your-auth-host/auth/oauth/token
Content-Type: application/json
{
"provider": "google",
"id_token": "<token-from-provider-sdk>"
}
Providerprovider value
Googlegoogle
Appleapple

Response shape is the same as registration.

Terminal window
npm install @react-oauth/google

Wrap your app root with the Google OAuth provider. The GOOGLE_CLIENT_ID is the OAuth 2.0 Client ID from your GCP project:

providers.tsx
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):

login-page.tsx
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"
/>
);
}
Terminal window
npm install react-apple-signin-auth

Replace 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}
/>
);
}

ID tokens expire after 1 hour. Use the refresh token to get a new one without re-authenticating:

Terminal window
POST https://your-auth-host/auth/refresh
Content-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;
}
Terminal window
POST https://your-auth-host/auth/forgot-password
Content-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.

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.

If the user did not receive the email or the link expired, they can request a new one:

Terminal window
POST https://your-auth-host/auth/send-verification-email
Authorization: Bearer <id_token>

Response (200 OK):

{ "status": "ok" }

Returns 400 with code AUTH_VERIFICATION_FAILED if the email is already verified.

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:

Detect verification on
// Refresh when the user switches back to the tab
document.addEventListener("visibilitychange", async () => {
  if (document.visibilityState === "visible") {
    const user = getCurrentUser(); // your local user state
    if (user && !user.email_verified) {
      const tokens = await refreshToken(); // POST /auth/refresh
      const me = await fetchMe(tokens.id_token); // GET /auth/me
      if (me.email_verified) {
        // Update local state, hide banner
      }
    }
  }
});

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.

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):

Terminal window
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.

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.

{
"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.

There are two valid approaches depending on your security requirements:

ApproachLibrarySignature verifiedWhen to use
Full verificationfirebase-adminYesPublic-facing APIs, production backends
Trusted decodeManual JWT decodeNoInternal services that trust TIE over HTTPS
Verify tokens within
$ npm install firebase-admin
import { initializeApp } from "firebase-admin/app";
import { getAuth } from "firebase-admin/auth";

// Initialize once. Project ID must match the `aud` claim in TIE tokens.
// Staging: "mv-stg-tie-core". Ask the TIE team for the production project ID.
initializeApp({ projectId: "<gcp-project-id>" });

export async function verifyTieToken(idToken: string) {
  const decoded = await getAuth().verifyIdToken(idToken);
  return {
    uid: decoded.uid,                // Firebase/GIP UID (same as `sub`)
    tieUserId: decoded.tie_user_id,  // TIE canonical user UUID
    email: decoded.email,
  };
}

Get current user:

Terminal window
GET https://your-auth-host/auth/me
Authorization: Bearer <id_token>

Returns the same user object from the login response.

Update profile:

Terminal window
PATCH https://your-auth-host/auth/me
Authorization: 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.

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.

RuleValue
Length3-30 characters
Allowed charactersLowercase letters, numbers, hyphens (-), underscores (_)
CaseAuto-lowercased (e.g. JaneDoe becomes janedoe)
Reserved wordsadmin, system, support, root, me, user, login, register, signup, logout, settings, help, info, api, null, undefined, moderator, administrator, username

Check if a username is available before claiming. No authentication required.

Terminal window
GET https://your-auth-host/auth/username/available?username=janedoe

With tenant scoping (pass the internal tenant UUID, not the GIP tenant ID):

Terminal window
GET https://your-auth-host/auth/username/available?username=janedoe&tenant_id=<tenant-uuid>

Response (200 OK):

{
"username": "janedoe",
"available": true
}

Claim a username for the authenticated user. This is a one-time operation — once claimed, it cannot be changed.

Terminal window
POST https://your-auth-host/auth/username/claim
Authorization: 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:

CodeStatusWhen
USER_USERNAME_TAKEN409Another user in the same tenant already claimed this username
USER_USERNAME_ALREADY_CLAIMED409You already have a username (cannot change it)
USER_USERNAME_INVALID422Username fails validation (too short, invalid characters, reserved word)

The username field is included in all user responses: GET /auth/me, login, registration, and OAuth sign-in responses. It is null until claimed.

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."
}
CodeStatusWhen
AUTH_TOKEN_MISSING401No Authorization header provided
AUTH_TOKEN_INVALID401Token expired, invalid signature, or wrong audience
AUTH_INVALID_CREDENTIALS401Wrong email/password at login
AUTH_ACCOUNT_DISABLED403User account has been suspended
AUTH_ADMIN_REQUIRED403Endpoint requires admin role
AUTH_EMAIL_EXISTS409A user with this email already exists
AUTH_REGISTRATION_FAILED400Registration failed due to invalid input
AUTH_OAUTH_FAILED401OAuth provider token is invalid
AUTH_OAUTH_PROVIDER_NOT_SUPPORTED400Provider is not google or apple
AUTH_REFRESH_FAILED401Refresh token is invalid or expired
AUTH_VERIFICATION_FAILED400Email is already verified, or verification email failed to send
USER_USERNAME_TAKEN409Username already claimed by another user in the same tenant
USER_USERNAME_ALREADY_CLAIMED409User already has a username (one-time claim)
USER_USERNAME_INVALID422Username fails validation rules
EndpointMethodAuthDescription
/auth/registerPOSTNoRegister with email/password
/auth/loginPOSTNoLogin with email/password
/auth/oauth/tokenPOSTNoOAuth sign-in (Google/Apple)
/auth/refreshPOSTNoRefresh an expired token
/auth/forgot-passwordPOSTNoSend password reset email
/auth/send-verification-emailPOSTYesRe-send email verification link
/auth/meGETYesGet current user profile
/auth/mePATCHYesUpdate own profile
/auth/username/availableGETNoCheck username availability
/auth/username/claimPOSTYesClaim a username (one-time)