xhostddocs
Console ↗
On this page

Sign in with Google

xhostd runs the Google sign-in dance and hands your app a signed identity token. Your app verifies it and decides access.

How it works

xhostd is an identity provider, not a gatekeeper. It runs the Google login dance and, on success, sets a cookie named __Host-xhost_id on your channel's hostname. That cookie is a signed JWT carrying the visitor's identity. Nothing is blocked at the edge — every request reaches your app. Your app reads the cookie, verifies its signature against xhostd's public keys, and decides for itself whether to allow the request.

This gives you full control: layer your own password/SMS login, allowlists, roles, or paid tiers on top — or ignore /xhost-auth/* entirely and run your own auth. xhostd gates nothing.

Zero config. /xhost-auth/* (login, logout, whoami) works on every deployed channel with no activation step. There is no protected-paths list to set — gating lives entirely in your app code.

After Google sign-in, xhostd sets __Host-xhost_id — an HttpOnly, Secure, host-only cookie (the __Host- prefix forces Path=/ and no Domain, so the cookie is never sent to another host). Its value is an RS256-signed JWT with these claims:

__Host-xhost_id claims
{
  "iss":   "https://auth.xhostd.com",
  "aud":   "your-app.xhostd.app",   // your channel's exact hostname
  "sub":   "1078...",               // stable Google user id
  "email": "visitor@gmail.com",
  "name":  "Visitor Name",
  "picture": "https://lh3.googleusercontent.com/...",  // optional; absent when Google sends none
  "iat":   1750000000,
  "exp":   1750086400
}
__Host-xhost_id is a reserved cookie name. Do not set it or read it as a raw value from your app — always verify it (below).

On each request that needs a user, verify the cookie against xhostd's published public keys. The rules are the same in every language:

  • JWKS URL: https://auth.xhostd.com/xhost-auth/jwks (fetch and cache it).
  • Pin RS256. Read kid from the token header to select the key — never let the token's own alg choose (prevents alg-confusion).
  • Require iss == "https://auth.xhostd.com", aud == "<your-host>" (your exact hostname — different for prod vs. staging), and exp not in the past.
  • On success, use sub as the primary key for your own user records; email can change.
Use a real JWT library that verifies signatures. Never use decode-only helpers (jwt-decode, jwt.decode(..., verify=False)) — they do not check the signature and will accept forged tokens.

Python — PyJWT

auth.py
import jwt
from jwt import PyJWKClient

ISSUER = "https://auth.xhostd.com"
AUDIENCE = "your-app.xhostd.app"            # your exact channel hostname
_jwks = PyJWKClient("https://auth.xhostd.com/xhost-auth/jwks")

def current_user(request):
    token = request.cookies.get("__Host-xhost_id")
    if not token:
        return None
    try:
        key = _jwks.get_signing_key_from_jwt(token).key
        claims = jwt.decode(
            token, key,
            algorithms=["RS256"],           # pin RS256
            issuer=ISSUER,
            audience=AUDIENCE,
        )
    except jwt.InvalidTokenError:
        return None
    return {"sub": claims["sub"], "email": claims["email"], "name": claims.get("name")}

Node — jsonwebtoken + jwks-rsa

auth.js
const jwt = require("jsonwebtoken");
const jwksClient = require("jwks-rsa");

const ISSUER = "https://auth.xhostd.com";
const AUDIENCE = "your-app.xhostd.app";     // your exact channel hostname
const jwks = jwksClient({
  jwksUri: "https://auth.xhostd.com/xhost-auth/jwks",
  cache: true, cacheMaxAge: 10 * 60 * 1000, rateLimit: true,
});
const getKey = (header, cb) =>
  jwks.getSigningKey(header.kid, (e, k) => cb(e, e ? null : k.getPublicKey()));

function currentUser(req) {
  return new Promise((resolve) => {
    const m = (req.headers.cookie || "").match(/(?:^|;\s*)__Host-xhost_id=([^;]+)/);
    if (!m) return resolve(null);
    jwt.verify(
      decodeURIComponent(m[1]), getKey,
      { algorithms: ["RS256"], issuer: ISSUER, audience: AUDIENCE },  // pin RS256
      (err, claims) =>
        resolve(err ? null : { sub: claims.sub, email: claims.email, name: claims.name }),
    );
  });
}

Go — go-oidc

auth.go
import (
    "context"
    "net/http"
    "github.com/coreos/go-oidc/v3/oidc"
)

const issuer = "https://auth.xhostd.com"
const audience = "your-app.xhostd.app"      // your exact channel hostname

// keySet caches xhostd's JWKS; build once at startup.
var keySet = oidc.NewRemoteKeySet(context.Background(),
    "https://auth.xhostd.com/xhost-auth/jwks")
var verifier = oidc.NewVerifier(issuer, keySet, &oidc.Config{
    ClientID:          audience,           // checks aud
    SupportedSigningAlgs: []string{"RS256"}, // pin RS256
})

func currentUser(r *http.Request) (map[string]any, bool) {
    c, err := r.Cookie("__Host-xhost_id")
    if err != nil {
        return nil, false
    }
    tok, err := verifier.Verify(r.Context(), c.Value)
    if err != nil {
        return nil, false
    }
    var claims map[string]any
    if err := tok.Claims(&claims); err != nil {
        return nil, false
    }
    return claims, true
}

Gate your routes

With a verifier in hand, gate whatever needs a user. If verification fails, redirect browsers to login or return 401/403 for API calls. Restricting which signed-in users may proceed (allowlists, paid tiers, team membership) is your app's job — check claims.email or claims.sub.

Flask — gate + email-domain allowlist
from flask import request, redirect, Response

@app.get("/admin")
def admin():
    user = current_user(request)
    if not user:
        return redirect(f"/xhost-auth/login?return_to={request.path}")
    if not user["email"].endswith("@acme.com"):
        return Response("Access denied", status=403)
    return render_admin(user)

Persist the user in your DB

Important. xhostd does NOT keep a list of who signed in. If you want a user table, write it into your own DB on first sight, keyed by sub (Google's stable identifier — emails can change).
Flask + SQLAlchemy
user = current_user(request)   # verified claims, see above
db.session.execute(
    insert(User)
    .values(sub=user["sub"], email=user["email"], name=user["name"])
    .on_conflict_do_update(
        index_elements=["sub"],
        set_={"email": user["email"], "name": user["name"],
              "last_seen_at": func.now()},
    )
)

Client-side / SPA apps

The cookie is HttpOnly, so browser JS cannot read it. Call GET /xhost-auth/whoami — the cookie is sent automatically same-origin and the gateway returns the verified identity:

index.html
const me = await fetch("/xhost-auth/whoami").then(r => r.json());
// { logged_in: true, email, name, sub }
//   or { logged_in: false, login_url }
if (me.logged_in) { document.body.dataset.email = me.email; }
else { window.location = me.login_url; }

Send signed-out users to /xhost-auth/login?return_to=<path>. After Google → finalize → cookie set → redirect to return_to. For sign-out: /xhost-auth/logout?return_to=/.

HTML
<a href="/xhost-auth/login?return_to=/admin">Sign in</a>
<a href="/xhost-auth/logout?return_to=/">Sign out</a>

Security model

  • The cookie JWT is signed with xhostd's private key; your app verifies it with the public key from the JWKS endpoint. Apps can verify but cannot forge an xhostd token — signature forgery stays impossible even if your app container is compromised.
  • The __Host- prefix forces the cookie host-only (Secure, Path=/, no Domain), so a token minted for one channel host is never sent to another. The aud claim (= hostname) adds defense-in-depth against a misrouted token.
  • xhostd unconditionally strips inbound X-Xhost-* request headers at the edge, so a client can never spoof identity headers. Read identity from the verified cookie, not headers.
  • Google ID tokens are verified cryptographically (RS256, Google's JWKS) during the dance, before the cookie is minted.
  • Multiple login sources? Verify each issuer against its own keys: iss: https://auth.xhostd.com via the JWKS above, your own issuer via your own key. Same claim shape, no trust shared.

Known limitations

  • xhostd's built-in dance is Google only. For GitHub / Apple / magic links / SMS, run your own login — the cookie model lets you mix yours with xhostd's.
  • Anyone with a Google account can complete the dance — no built-in allowlist. Filter in your app code against claims.email.
  • xhostd cannot tell you who has accounts on your app — keep your own DB.

What this isn't

  • It's not a Google Workspace integration — you can't request Drive/Gmail/Calendar scopes.
  • It's not SSO across xhostd channels — the cookie is host-only, so every channel is its own login surface.

Privacy · Terms

Enter a topic, task, or tool name.

Public documentation only · Search stays in your browser.