跳到主要内容

Getting started

This page takes you from zero to a verified JWT in about fifteen minutes on your own machine. It's a local demo, not a production deployment — one docker run command with the mobile app on the same LAN, no TLS, no reverse proxy. For production recipes (self- terminated TLS, compose + Caddy with Let's Encrypt, or Kubernetes), see deploy/README.md after you've walked through this.

1. Run ZTXBAS

ZTXBAS ships as a signed container image at ghcr.io/corezt/ztxbas. The image is signed with cosign; verifying the signature is optional for a demo but recommended before any real install (see deploy/README.md).

You'll also need an SMTP relay reachable from the container — the enrollment step in section 3 sends a link by email.

Prep an env file. Copy the shipped example and fill in a couple of values:

cp env.txt .env

Find your machine's LAN IP (the phone must be able to reach it):

ip -4 addr show | grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+' | grep -v '^127\.'

Edit .env and set:

ZTXBAS_PUBLIC_URL=http://<lan-ip>:8443
ZTXBAS_CONSOLE_LISTEN_ADDR=0.0.0.0:8080
ZTXBAS_ALLOW_INSECURE_CONSOLE=1
ZTXBAS_SMTP_HOST=smtp.example.com
ZTXBAS_SMTP_PORT=587
ZTXBAS_SMTP_USER=your-smtp-user
ZTXBAS_SMTP_PASS=your-smtp-pass
ZTXBAS_SMTP_FROM=ztxbas@yourdomain.com

ZTXBAS_PUBLIC_URL is baked into every enrollment link and QR code, so it has to be an address the phone can actually reach. Loopback won't work.

ZTXBAS_CONSOLE_LISTEN_ADDR=0.0.0.0:8080 binds the console inside the container to all interfaces so Docker's port mapping can reach it; ZTXBAS_ALLOW_INSECURE_CONSOLE=1 tells the startup fail-safe that this is intentional. The console still isn't publicly exposed — -p 127.0.0.1:8080:8080 in the docker command restricts it to host loopback.

Run it:

docker run --rm --name ztxbas \
--env-file .env \
-p <lan-ip>:8443:8443 \
-p 127.0.0.1:8080:8080 \
-p 9443:9443/udp \
-v ztxbas-data:/var/lib/ztxbas \
--cap-drop ALL --security-opt no-new-privileges \
--read-only --tmpfs /tmp:size=16m \
ghcr.io/corezt/ztxbas:latest

Three ports are involved:

PortBound toPurpose
8443/tcpLAN IPRP-facing API — your app / SDK calls this.
8080/tcpLoopbackAdmin console .
9443/udpAll ifacesMobile-app transport. Publish directly — this
is a custom framed protocol; HTTP proxies can't
forward it.

The admin console stays on loopback because you open it in a browser on the same host. If you're SSH'd into a remote host, add -L 8080:localhost:8080 to your SSH command and open http://127.0.0.1:8080 locally.

Health check:

curl -s http://<lan-ip>:8443/health
# {"status":"ok","version":"1.0.0"}

2. Log in and create an application

An application is your RP. It gets an id and an HMAC secret that you sign API requests with.

Grab the first-boot admin password from the container logs:

docker logs ztxbas 2>&1 | grep -i breakglass

Open 127.0.0.1:8080, log in with that password, and set a proper one when prompted (the breakglass password is single-use).

From the console: Applications → New application, give it a name, and you'll be shown the app id and HMAC secret once. Save both - the secret isn't recoverable.

Prefer the CLI? The same thing works via docker exec:

docker exec ztxbas ztxbas app create "Quickstart App"
# Application created.
# ID: app_a1b2c3d4e5
# Name: Quickstart App
# HMAC secret: 5f6e7d8c9b0a...

3. Register an origin

An origin is a scheme://host[:port] your app authenticates from. It's shown to the user on their phone during approval - this is the anti-phishing anchor.

Easiest path: in the console, open the application you just created and add the origin (https://app.example.com) with a display name (e.g. Example App). The display name is what the user sees on their phone.

If you'd rather do it from code, the SDKs handle the HMAC signing for you - signing by hand is fiddly (see HMAC signing for the canonical form).

4. Register a user and challenge

Pick your language. All three run the same flow: register user → create challenge → poll → get a verified JWT back.

Go

c, _ := ztxbas.New("http://127.0.0.1:8443", "app_a1b2c3d4e5", "5f6e7d8c9b0a...")
_, _ = c.RegisterUser(ctx, ztxbas.RegisterUserRequest{Email: "alice@example.com"})
ch, _ := c.CreateChallenge(ctx, ztxbas.CreateChallengeRequest{
UserEmail: "alice@example.com",
Origin: "https://app.example.com",
})
claims, _ := c.PollChallenge(ctx, ch.ChallengeID)
fmt.Println(claims.Email, claims.Origin)

Node/TypeScript

const c = new Client("http://127.0.0.1:8443", "app_a1b2c3d4e5", "5f6e7d8c9b0a...");
await c.registerUser({ email: "alice@example.com" });
const ch = await c.createChallenge({
user_email: "alice@example.com",
origin: "https://app.example.com",
});
const claims = await c.pollChallenge(ch.challenge_id);
console.log(claims.email, claims.origin);

Python

c = Client("http://127.0.0.1:8443", "app_a1b2c3d4e5", "5f6e7d8c9b0a...")
c.register_user("alice@example.com")
ch = c.create_challenge("alice@example.com", "https://app.example.com")
claims = c.poll_challenge(ch["challenge_id"])
print(claims.email, claims.origin)

For a real deployment, swap http://127.0.0.1:8443 for your TLS front-end (https://ztxbas.example.com).

5. What just happened

  1. The user got an enrollment email; they installed the ZTXBAS authenticator app and paired their phone. The app talks to ZTXBAS server over the UDP port you exposed on 9443.
  2. Your call to create_challenge pushed a biometric prompt to their device showing "Example App" (the origin's display name).
  3. They approved with fingerprint / face.
  4. ZTXBAS minted an ES256 JWT bound to alice@example.com and https://app.example.com.
  5. Your SDK fetched the JWKS from /.well-known/jwks.json, verified the signature, and returned the claims.

Ready-to-run quickstarts

Full end-to-end demos in each language:

Each is about 30 lines and exercises the same flow you see above.

Next steps