xhost
Sign in

Sign in with Google

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

How it works

xhost 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 xhost'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. xhost 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, xhost 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.com",   // your channel's exact hostname
  "sub":   "1078...",               // stable Google user id
  "email": "visitor@gmail.com",
  "name":  "Visitor Name",
  "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 xhost's published public keys. The rules are the same in every language:

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.com"            # 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.com";     // 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.com"      // your exact channel hostname

// keySet caches xhost'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. xhost 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

Known limitations

What this isn't

Privacy · Terms