What We’re Testing
QuickZTNA uses a two-token authentication model (handlers/auth.ts):
- Access token: ES256 JWT, 1 hour TTL (
ACCESS_TOKEN_TTL = 3600) - Refresh token: Opaque hex string, 30 day TTL (
REFRESH_TOKEN_TTL = 30 * 86400), stored in Valkey (sessionskeyspace) - Refresh endpoint:
POST /api/auth/refreshwith{ "refresh_token": "..." }— rotates the refresh token (old one deleted, new one issued) - Logout:
POST /api/auth/logout— deletes the refresh token from Valkey
The CLI (cmd_login.go) saves tokens to ~/.config/ztna/tokens.json. The frontend stores them in localStorage and auto-refreshes 5 minutes before expiry.
Your Test Setup
| Machine | Role |
|---|---|
| ⊞ Win-A | API testing via curl + dashboard browser testing |
ST1 — Access Token Expiry Verification
What it verifies: Access tokens expire after 1 hour and are rejected after expiry.
Steps:
- Log in and capture the access token:
RESPONSE=$(curl -s -X POST https://login.quickztna.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"YOUR_EMAIL","password":"YOUR_PASSWORD"}')
TOKEN=$(echo $RESPONSE | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])")
EXPIRES=$(echo $RESPONSE | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['expires_in'])")
echo "Token expires in: ${EXPIRES}s"
Expected: Token expires in: 3600s
- Decode the JWT to verify the
expclaim:
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
Expected JWT payload:
{
"sub": "user-id-here",
"email": "your@email.com",
"exp": 1742299200,
"iat": 1742295600
}
- Verify
exp - iat = 3600(1 hour).
Pass: expires_in: 3600. JWT exp claim is exactly 3600 seconds after iat.
Fail / Common issues:
expires_inis different — the constant may have changed. Checkauth.tsline 48.- Base64 decode fails — JWT segments use URL-safe base64. Add
==padding if needed.
ST2 — Refresh Token Rotation
What it verifies: The refresh endpoint issues a new token pair and invalidates the old refresh token.
Steps:
- Log in and capture both tokens:
RESPONSE=$(curl -s -X POST https://login.quickztna.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"YOUR_EMAIL","password":"YOUR_PASSWORD"}')
TOKEN=$(echo $RESPONSE | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])")
REFRESH=$(echo $RESPONSE | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['refresh_token'])")
echo "Refresh token: $REFRESH"
- Use the refresh token to get a new pair:
NEW_RESPONSE=$(curl -s -X POST https://login.quickztna.com/api/auth/refresh \
-H "Content-Type: application/json" \
-d "{\"refresh_token\":\"$REFRESH\"}")
echo $NEW_RESPONSE | python3 -m json.tool
Expected response:
{
"success": true,
"data": {
"token": "eyJ...(new access token)",
"refresh_token": "...(new refresh token, different from old)",
"expires_in": 3600
}
}
- Try the old refresh token again (should be invalidated):
curl -s -X POST https://login.quickztna.com/api/auth/refresh \
-H "Content-Type: application/json" \
-d "{\"refresh_token\":\"$REFRESH\"}" | python3 -m json.tool
Expected:
{
"success": false,
"error": {
"code": "INVALID_TOKEN",
"message": "Invalid or expired refresh token"
}
}
Pass: New token pair issued. Old refresh token is rejected (rotation). New refresh token is different from old.
Fail / Common issues:
- Old refresh token still works — the backend may not be deleting old tokens from Valkey. Security concern — report this.
MISSING_TOKEN(400) — therefresh_tokenfield is missing from the request body.
ST3 — Logout Invalidates Refresh Token
What it verifies: POST /api/auth/logout deletes the refresh token so it can’t be reused.
Steps:
- Log in and capture tokens:
RESPONSE=$(curl -s -X POST https://login.quickztna.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"YOUR_EMAIL","password":"YOUR_PASSWORD"}')
TOKEN=$(echo $RESPONSE | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])")
REFRESH=$(echo $RESPONSE | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['refresh_token'])")
- Log out:
curl -s -X POST https://login.quickztna.com/api/auth/logout \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"refresh_token\":\"$REFRESH\"}" | python3 -m json.tool
- Try to use the refresh token:
curl -s -X POST https://login.quickztna.com/api/auth/refresh \
-H "Content-Type: application/json" \
-d "{\"refresh_token\":\"$REFRESH\"}" | python3 -m json.tool
Expected: Refresh fails with INVALID_TOKEN — the token was deleted on logout.
Pass: After logout, the refresh token is invalid. No way to get new access tokens without re-authenticating.
ST4 — CLI Token Storage and Status
What it verifies: The CLI stores tokens in tokens.json and uses them for authenticated commands.
Steps:
- On ⊞ Win-A , log in:
ztna login --interactive
- Check that tokens were saved:
# Windows:
type %USERPROFILE%\.config\ztna\tokens.json
# Or:
cat ~/.config/ztna/tokens.json
Expected: A JSON file containing access_token and refresh_token fields.
- Verify authenticated status:
ztna status
Should show Authenticated: true.
- Log out:
ztna logout
- Check tokens again:
ztna status
Should show Authenticated: false and the hint (run 'ztna login' to authenticate).
Pass: Tokens saved after login, Authenticated: true. After logout, Authenticated: false.
Fail / Common issues:
tokens.jsondoesn’t exist — the CLI may not have write permissions to~/.config/ztna/. Check directory permissions.Authenticated: trueafter logout — tokens file may not have been cleared. Delete it manually:rm ~/.config/ztna/tokens.json.
ST5 — Expired Access Token Rejected by API
What it verifies: An expired JWT access token is rejected by protected API endpoints.
Steps:
- Create a deliberately expired JWT (or wait 1 hour after login):
For quick testing, use an old token from a previous session, or forge a short-lived one. The simplest approach:
# Get a valid token
TOKEN=$(curl -s -X POST https://login.quickztna.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"YOUR_EMAIL","password":"YOUR_PASSWORD"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])")
# Use it immediately — should work
curl -s https://login.quickztna.com/api/auth/me \
-H "Authorization: Bearer $TOKEN" | python3 -c "import sys,json; d=json.load(sys.stdin); print('Status:', 'ok' if d['success'] else 'failed')"
- Now try with a clearly invalid/expired token:
curl -s https://login.quickztna.com/api/auth/me \
-H "Authorization: Bearer expired.token.here" | python3 -m json.tool
Expected response:
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or expired token"
}
}
Pass: Valid token returns user data. Invalid/expired token returns 401 UNAUTHORIZED.
Summary
| Sub-test | What it proves | Pass condition |
|---|---|---|
| ST1 | Access token TTL | expires_in: 3600, JWT exp-iat = 3600 |
| ST2 | Refresh rotation | New pair issued, old refresh invalidated |
| ST3 | Logout invalidation | Refresh token rejected after logout |
| ST4 | CLI token storage | Tokens in tokens.json, status reflects auth state |
| ST5 | Expired token rejection | Invalid token returns 401 |