TAuth

TAuth Usage Guide

This document is the authoritative guide for operators and front‑end teams integrating against a TAuth deployment. It explains how to run the service, how sessions work, and how to connect a browser application using either the provided helper script or direct HTTP calls.

For a deep dive into internal architecture and implementation details, see ARCHITECTURE.md. For confident‑programming and refactor policies, see POLICY.md and docs/refactor-plan.md.


1. What TAuth provides

TAuth sits between Google Identity Services (GIS) and your product UI:

Once TAuth is running for a given registrable domain, any app on that domain (or its subdomains) can rely on the HttpOnly session cookies instead of storing tokens in localStorage or JavaScript memory.


2. Running the service

2.1 Binary layout

The tauth binary lives under cmd/server in this repository. You can:

The binary reads configuration exclusively from a YAML file (default config.yaml). Use tauth --config=/path/to/config.yaml or export TAUTH_CONFIG_FILE to point at a different file; no other environment variables or CLI flags are required.

2.2 Core configuration

config.yaml must include the server-level keys below plus at least one tenant:

Key Purpose Example
listen_addr HTTP listen address :8080
database_url Refresh store DSN sqlite:///data/tauth.db
enable_cors Enable CORS for cross-origin UIs true / false
cors_allowed_origins Allowed origins when CORS is enabled (include your UI origins and https://accounts.google.com) ["https://app.example.com","https://accounts.google.com"]
enable_tenant_header_override Allow X-TAuth-Tenant overrides (dev/local only) true / false
tenants Array of tenant entries (see README §5.1 for schema) [...]

Key notes:

2.3 Example: hosted deployment

This example mirrors the README but focuses on the minimum you need to host TAuth at https://auth.example.com for a product UI at https://app.example.com:

cat > config.yaml <<'YAML'
server:
  listen_addr: ":8443"
  database_url: "sqlite:///data/tauth.db"
  enable_cors: true
  cors_allowed_origins:
    - "https://app.example.com"
    - "https://accounts.google.com"
  enable_tenant_header_override: false

tenants:
  - id: "prod"
    display_name: "Production Tenant"
    allowed_hosts:
      - "auth.example.com"
      - "https://app.example.com"
    google_web_client_id: "your_web_client_id.apps.googleusercontent.com"
    jwt_signing_key: "replace-with-your-tenant-signing-key"
    cookie_domain: ".example.com"
    session_ttl: "15m"
    refresh_ttl: "1440h"
    nonce_ttl: "5m"
    allow_insecure_http: false
YAML

tauth --config=config.yaml

Run this behind TLS so the service issues Secure cookies and the browser accepts them.

When migrating an existing tenant that expects the legacy cookie names (app_session, app_refresh), set the session_cookie_name / refresh_cookie_name fields inside the tenant block. These fields are always required—choose unique names per tenant to avoid collisions when multiple tenants share localhost. Legacy stacks (such as Gravity) can keep app_session / app_refresh, but doing so means any other tenant using the same names will overwrite those cookies.

2.4 Example: local quick‑start (Docker Compose)

For a full local stack (TAuth + demo UI) without installing Go:

  1. cd examples/docker-compose
  2. Copy the environment template: cp .env.tauth.example .env.tauth
  3. Copy the config template: cp config.yaml.example config.yaml
  4. Edit .env.tauth (set TAUTH_CONFIG_FILE=/config/config.yaml and the per-tenant TAUTH_GOOGLE_WEB_CLIENT_ID* / TAUTH_*_JWT_SIGNING_KEY values).
  5. Edit config.yaml and replace the placeholder Google OAuth client with one registered for http://localhost:8000 and http://localhost:8080 (or keep the environment variable references from step 4).
  6. Start the stack: docker compose up --build
  7. Visit http://localhost:8000 for the demo UI. It talks to TAuth at http://localhost:8080.

Stop the stack with docker compose down. The tauth_data volume holds the SQLite database, and config.yaml stays next to the compose file for future edits.

2.5 Preflight validation (pre-start)

Use the preflight command to validate configuration and emit a redacted effective-config report before you launch the service:

tauth preflight --config=config.yaml

The report includes effective server settings, per-tenant cookie names and TTLs, derived SameSite modes, and JWT signing key fingerprints (never raw keys). Redacted reports still emit allowed_host_hashes and jwt_signing_key_fingerprint so external validators can compare secrets without exposing them. To include the raw allowed_hosts list, pass --include-hosts.

The JSON payload is versioned and shaped as:

The preflight builder is generalized under github.com/tyemirov/utils/preflight with a Viper-based adapter (github.com/tyemirov/utils/preflight/viperconfig) for services that load YAML configs and bind env vars through Viper.


3. Sessions and cookies

TAuth works with two cookies:

Your product should:


The simplest way to use TAuth from the browser is through the helper served at /static/auth-client.js. It exports four globals:

For backend services written in Go, use the pkg/sessionvalidator package described in section 6.8 to validate app_session cookies.

4.1 Loading the helper

On your product site, include the script from your TAuth origin:

<script
  src="https://auth.example.com/static/auth-client.js"
  data-tenant-id="tenant-admin"
></script>

If your UI and TAuth share a host (for example both under https://app.example.com), you can serve it directly from that origin instead.

4.2 Initialising on page load

Call initAuthClient once during startup, after the script loads:

<script>
  // Optional: override tenant dynamically when the page knows which tenant to use.
  setAuthTenantId("tenant-admin");
  initAuthClient({
    baseUrl: "https://auth.example.com",
    tenantId: "demo", // optional override for shared-host dev setups
    onAuthenticated(profile) {
      renderDashboard(profile);
    },
    onUnauthenticated() {
      showSignInButton();
    },
  });
</script>

Behaviour:

4.3 Calling your own APIs with apiFetch

Wrap all authenticated HTTP requests through apiFetch:

async function loadProtectedData() {
  const response = await apiFetch("/api/data", { method: "GET" });
  if (!response.ok) {
    throw new Error("request_failed");
  }
  return response.json();
}

When a call returns 401, apiFetch:

  1. Sends POST /auth/refresh with credentials: "include".
  2. Retries the original request on success.
  3. Broadcasts "refreshed" events via BroadcastChannel (if available), allowing multiple tabs to stay in sync.

If refresh fails, pending requests reject and callers can treat this as “logged out”.

4.4 Logging out

Use logout() to terminate the session:

async function handleLogoutClick() {
  await logout();
  redirectToLanding();
}

The helper:

4.5 Selecting a tenant explicitly

Most deployments rely on hostnames to resolve tenants. When multiple tenants intentionally share the same host (for example, several apps pointing at localhost:8080), enable the TAuth server’s header override (--enable_tenant_header_override). Once enabled, the helper tags /me and /auth/* calls with either your explicit tenantId or, when omitted, the current page origin so shared-host setups continue to function even if certain requests omit Origin. You can still pin a specific tenant explicitly by passing tenantId to initAuthClient:

initAuthClient({
  baseUrl: "https://auth-dev.example.com",
  tenantId: "team-blue",
  onAuthenticated: hydrateDashboard,
  onUnauthenticated: showGoogleButton,
});

The helper automatically attaches X-TAuth-Tenant: team-blue (or the current page origin when no ID is supplied) to /me, /auth/nonce, /auth/google, /auth/refresh, and logout requests while leaving your own API traffic alone. Switch tenants by reinitialising with a different tenantId (or prefer separate hosts when possible). The override never bypasses host validation — TAuth still checks that the HTTP Host header appears in some tenant definition and rejects requests from unlisted hosts even if the header is present.


5. Google Identity Services flow

TAuth assumes a GIS Web client using the popup flow. A nonce protects each sign‑in exchange.

5.1 Configure GIS

  1. Create (or reuse) a Google OAuth Web client.
  2. Add all product origins (for example https://app.example.com) and the TAuth origin (for example https://auth.example.com) to Authorized JavaScript origins.
  3. Load the GIS script:

    <script src="https://accounts.google.com/gsi/client" async defer></script>
    <div id="googleSignIn"></div>
    

5.2 Nonce and credential exchange

The required sequence for custom clients is:

  1. NoncePOST /auth/nonce
    • Returns { "nonce": "<random>" }.
  2. Initialize GIS with the nonce:
    • google.accounts.id.initialize({ client_id, nonce, ux_mode: "popup", callback }).
  3. Show the button / popup via GIS APIs.
  4. Exchange credential – when GIS invokes your callback with response.credential:
    • Call POST /auth/google with JSON { "google_id_token": "<response.credential>", "nonce_token": "<same nonce>" } and credentials: "include".
  5. TAuth:
    • Validates the ID token against the resolved tenant’s google_web_client_id.
    • Verifies the nonce (raw or hashed) and the issuer.
    • Issues app_session and app_refresh cookies.
    • Returns a profile JSON payload.

You must fetch a fresh nonce for every sign‑in attempt. TAuth invalidates a nonce as soon as it is used.

When using auth-client.js or the mpr‑ui header component, this flow is handled internally; you only need to surface the Google button and configure your client ID.


6. HTTP endpoints

This section documents the public HTTP surface from a client’s perspective. See ARCHITECTURE.md for a stable contract summary and versioning notes. These endpoints are served exclusively by the TAuth server; consuming applications should call them, not reimplement them.

6.1 POST /auth/nonce

Issues a one‑time nonce for the next GIS exchange.

6.2 POST /auth/google

Verifies a Google ID token and mints cookies.

Common failure cases:

6.3 GET /me

Returns the profile associated with the current session.

6.4 POST /auth/refresh

Rotates the refresh token and mints a new access cookie.

After a successful refresh, call /me again or rely on auth-client.js to hydrate the profile.

6.5 POST /auth/logout

Revokes the refresh token and clears cookies.

Clients should treat this as “signed out” regardless of prior state.

6.6 GET /static/auth-client.js

Serves the browser helper described in section 4.

6.7 GET /demo

Optional demo page shipped with the repository. Intended for local development only.


6.8 Validating sessions from other Go services

Downstream Go services that share the TAuth cookie domain can validate app_session cookies directly using the pkg/sessionvalidator package. This is the recommended way to enforce authentication and read identity information without duplicating JWT logic. If your service can read the same config.yaml as TAuth, call LoadTenantAuthConfig to derive the tenant’s signing key, issuer, and cookie names before constructing a validator.

6.8.1 Basic validator setup

Add the module to your Go service and construct a validator at startup:

import (
    "os"

    "github.com/tyemirov/tauth/pkg/sessionvalidator"
)

func newSessionValidator() (*sessionvalidator.Validator, error) {
    signingKey := []byte(os.Getenv("TAUTH_NOTES_JWT_SIGNING_KEY"))
    return sessionvalidator.New(sessionvalidator.Config{
        SigningKey: signingKey,
        Issuer:     "tauth",
        // CookieName: optional; defaults to "app_session".
    })
}

The configuration mirrors your TAuth deployment:

The constructor validates configuration up front and returns a typed error if required fields are missing.

6.8.2 Gin middleware integration

For Gin-based services, use the built-in middleware to protect routes and attach claims to the context:

import (
    "log"

    "github.com/gin-gonic/gin"
    "github.com/tyemirov/tauth/pkg/sessionvalidator"
)

func main() {
    validator, err := newSessionValidator()
    if err != nil {
        log.Fatalf("invalid validator configuration: %v", err)
    }

    router := gin.Default()
    router.Use(validator.GinMiddleware(sessionvalidator.DefaultContextKey))

    router.GET("/me", func(context *gin.Context) {
        claimsValue, exists := context.Get(sessionvalidator.DefaultContextKey)
        if !exists {
            context.AbortWithStatus(http.StatusUnauthorized)
            return
        }
        claims := claimsValue.(*sessionvalidator.Claims)
        context.JSON(http.StatusOK, map[string]interface{}{
            "user_id":    claims.GetUserID(),
            "user_email": claims.GetUserEmail(),
            "display":    claims.GetUserDisplayName(),
            "avatar_url": claims.GetUserAvatarURL(),
            "roles":      claims.GetUserRoles(),
        })
    })

    _ = router.Run()
}

Key points:

6.8.3 Manual validation flows

If you are not using Gin, or you need finer-grained control, use the lower-level helpers:

Example with net/http:

func handleProtectedRoute(response http.ResponseWriter, request *http.Request, validator *sessionvalidator.Validator) {
    claims, err := validator.ValidateRequest(request)
    if err != nil {
        http.Error(response, "unauthorized", http.StatusUnauthorized)
        return
    }
    // Use claims.* accessors here.
}

Using the shared validator keeps your services aligned with TAuth’s JWT format and validation rules, and avoids duplicating cryptographic or time-based logic across codebases.


7. Typical flows

7.1 First sign‑in

  1. User clicks “Sign in with Google”.
  2. UI calls /auth/nonce, configures GIS with the nonce, and shows the popup.
  3. GIS returns a credential; UI posts it to /auth/google.
  4. TAuth validates the token, issues cookies, returns profile JSON.
  5. UI renders signed‑in state and begins using apiFetch for protected calls.

7.2 Silent refresh

  1. An API call via apiFetch returns 401.
  2. apiFetch sends POST /auth/refresh with the refresh cookie.
  3. On success, it retries the original request and broadcasts "refreshed".
  4. UI continues to operate with renewed session cookies.

7.3 Logout

  1. User clicks “Sign out”.
  2. UI calls logout().
  3. TAuth revokes the refresh token and clears cookies.
  4. Helper broadcasts "logged_out"; all tabs transition to unauthenticated state.

8. Troubleshooting

Use this checklist when integrating:

For more detailed operational guidance, refer to the troubleshooting section in ARCHITECTURE.md.