A minimal self-hosted web portal that lets your own users mint (and optionally manage) their own Takumi Guard org-user tokens, using a Takumi Guard bot identity that you configure on the portal side.
It exists for customers who:
- need per-developer Takumi Guard tokens, typically to drop into
.npmrc, a CI runner secret, or another local tool config, and - can't or don't want to rely on MDM-style forced rollout to push a shared bot credential to every developer machine,
but can stand up a small internal web app that authenticates their own users with their existing identity provider and brokers a per-user token mint on their behalf.
The portal always lets the authenticated user mint a token. List and
revoke are auto-enabled when the bot has the corresponding Guard
permissions: when present, each user can see and revoke their own tokens
(and only their own, exact-matched by user_identifier). When absent, the
portal silently degrades to mint-only. See
Capability-driven UI below for how this is
detected.
What the portal looks like in production, deployed behind Google IAP for a single Google Workspace user:
The default landing page when the bot has list_* and revoke_* Guard
permissions. The signed-in identity (top right) drives the
user_identifier filter; only the caller's own tokens are listed, with a
per-row Revoke action.
The plaintext token is shown once. After this page is closed, the portal can never recover it; the user must copy it into their tooling now or revoke and re-mint.
When the bot only holds issue_takumi_guard_token (no list/revoke), the
landing page degrades to just the "Mint a new token" button. Same flow,
no list section.
flowchart LR
Dev([developer browser])
subgraph CustomerSide["Customer side (you operate this)"]
Portal["<b>guard-token-portal</b><br/>━━━━━━━━<br/>holds Guard bot id + API key<br/>holds per-user mint policy<br/>(adapter: IAP / Basic / custom)"]
end
subgraph ShishoSide["Shisho Cloud"]
API["Takumi API<br/>api.cloud.shisho.dev/v1<br/>/o/{orgID}/guard/org-user-tokens"]
end
Dev ==>|"1. authenticate (via configured IdP)<br/>+ mint / list-own / revoke-own"| Portal
Portal ==>|"2. bot-authenticated REST<br/>filtered to developer's user_identifier"| API
API -.->|"3. response<br/>(minted token / token list / revoke ack)"| Portal
Portal -.->|"4. token shown once /<br/>list re-filtered exact-match"| Dev
The bot credentials never leave the portal. Each authenticated developer
can mint, list, and revoke tokens only under their own
user_identifier. The minted token is what the developer pastes into
their own tool config (.npmrc, CI secret, etc.); from that point on the
tool calls Takumi Guard directly with the org-user token. It never
sees, and never needs, the bot credentials.
The portal's UI surface is controlled by the bot's Guard permissions, not by any config flag:
| Bot permission | What the user sees | What the portal calls |
|---|---|---|
issue_takumi_guard_token only |
Mint button only |
|
issue_takumi_guard_token + list/revoke |
Mint button + list of caller's own tokens with per-row revoke |
|
Detection happens per request: every load of / attempts a list call.
If Guard returns 403 the portal renders the mint-only view; if Guard
accepts the list the portal renders the manage view. Granting or revoking
the bot's list permission is therefore a Guard-side action; no portal
redeploy is needed.
When list/revoke are enabled, the portal still shows only the caller's own tokens. There is no admin view, even for organisation admins, and neither the list nor revoke handlers can be coaxed into operating on another user's token.
Two filters work together:
- Server-side prefix filter. The portal sends the caller's email as
user_identifierto Guard's list endpoint. Guard treats this as a prefix match (so the wire result set is bounded byO(your tokens), notO(org tokens)). - Client-side exact match. The portal then drops any returned row
whose
user_identifieris not byte-for-byte equal to the caller's email. This is the defence that closes the prefix-match gap: a row withuser_identifier = "alice@example.com.attacker"would pass Guard's prefix filter for"alice@example.com"but is rejected here.
Revoke uses the same list → exact-match lookup to find the target
token, so a forged token_id in the revoke form cannot reach a token
the caller does not own.
In Shisho Cloud, create a bot and assign it one of these roles depending on how much UI you want the portal to expose:
| Role | Portal UI surface |
|---|---|
| Takumi Guard Token Issuer | Mint only |
| Takumi Manager | Mint + per-user list + per-user revoke |
Either role gives the bot exactly what it needs; the difference is whether the portal also offers list/revoke. Detection is automatic (see Capability-driven UI), so switching roles later takes effect on the next page load. No portal redeploy needed.
Note the bot id (BT01…) and API key (shisho_apikey_…); they go into
PORTAL_BOT_ID and PORTAL_BOT_API_KEY in the next step.
Either run it locally (see Build & run (locally))
or deploy to Cloud Run with IAP (see
examples/cloudrun/ for a worked recipe
with Secret Manager + IAP + sha256-pinned base images).
A developer signs in, clicks Mint a new token, copies the plaintext
once, and pastes it into the tool that needs it: .npmrc, CI runner
secret, build-server credential store, etc. The plaintext is shown
exactly once; if it's lost, the developer just mints a new one (and
revokes the old one if list/revoke is enabled).
This module uses only the Go standard library: no web framework, no
template engine, no JS/Tailwind/build step. The whole portal is a single
statically-linked binary; HTML templates and CSS are embedded via embed.
Every shipped adapter is real code that lives in the repo and is reviewed
alongside the rest of the portal; there is no overlay file you copy over a
default. The same binary can run as basic-auth on a developer's laptop and
as IAP-aware on Cloud Run; you pick at deploy time with PORTAL_AUTH_MODE.
internal/auth/auth.go declares the contract:
type Authenticator interface {
Identify(r *http.Request) Identity
ChallengeUnauthenticated(w http.ResponseWriter, r *http.Request)
CanMintFor(caller Identity, target string) bool
CanManage(caller Identity, tokenUserIdentifier string) bool
}Shipped adapters:
PORTAL_AUTH_MODE |
File | Use case | Extra env vars |
|---|---|---|---|
basic (default) |
internal/auth/basic.go |
Local dev, smoke-testing against a real Guard tenant. Never deploy to production. | PORTAL_BASIC_AUTH_PASSWORD |
iap |
internal/auth/googlecloud_iap.go |
Cloud Run with Identity-Aware Proxy in front. Cryptographically verifies the IAP JWT (X-Goog-IAP-JWT-Assertion); does NOT trust X-Goog-Authenticated-User-Email. |
PORTAL_IAP_AUDIENCE |
- Write a sibling file in
internal/auth/(e.g.oidc.go) defining a type that implementsAuthenticatorand a private constructor returning(Authenticator, error). Read any env vars the adapter needs inside that constructor; keepbuild.godumb. - Add a
case "yourmode":to the switch inbuild.go. - Document the mode and its env vars in the table above.
main.go only calls auth.Build(), so it never needs to know which adapter
is in play.
| Env var | Required | Default | Description |
|---|---|---|---|
PORTAL_ORG_ID |
yes | - | Shisho Cloud organization id the bot belongs to. Interpolated into every API URL (/o/{orgID}/…). |
PORTAL_BOT_ID |
yes | - | Shisho Cloud bot id. Must hold issue_takumi_guard_token. List/revoke Guard permissions are optional, granting them auto-enables the manage UI (see Capability-driven UI). |
PORTAL_BOT_API_KEY |
yes | - | API key for the bot above. Combined with PORTAL_BOT_ID to form the Authorization: Bearer shisho_apikey_<key>:<bot_id> header on every API call. |
PORTAL_AUTH_MODE |
no | basic |
Which Authenticator adapter to use. See the table above. |
PORTAL_GUARD_API_BASE_URL |
no | https://api.cloud.shisho.dev/v1 |
Override for staging or self-hosted Shisho. |
PORTAL_LISTEN_ADDR |
no | :8080 |
host:port to bind. |
PORTAL_COOKIE_INSECURE |
no | unset | Set to 1 for plaintext HTTP local dev (turns off the Secure cookie flag). |
Adapter-specific env vars (e.g. PORTAL_BASIC_AUTH_PASSWORD) are read by
the adapter itself; see its file for the canonical list.
cd guard-token-portal
go build -o guard-token-portal .
# Defaults to PORTAL_AUTH_MODE=basic.
PORTAL_ORG_ID=your-org-id \
PORTAL_BOT_ID=BT01XXXX \
PORTAL_BOT_API_KEY=shisho_apikey_... \
PORTAL_BASIC_AUTH_PASSWORD='change-me' \
PORTAL_COOKIE_INSECURE=1 \
./guard-token-portalThen open http://localhost:8080 and sign in. The username you type is the
user_identifier that Guard will record on every token you mint.
| Target | Path |
|---|---|
| Google Cloud Run (with Secret Manager + IAP) | examples/cloudrun/ |
Each example bundles the infra commands and a Dockerfile, and points at the
adapter you should select with PORTAL_AUTH_MODE.
This repository is an example portal implementation only and has only been subjected to a minimum level of review. Before running it for real, verify that it satisfies your organisation's required security standards, and modify it where it does not.
The shipped Basic-auth adapter (PORTAL_AUTH_MODE=basic) is for local
debugging and smoke testing only. It accepts any username paired with
a single shared password, so anyone who reaches the portal can mint
tokens under any user_identifier they choose: it does not provide
per-user identity. For real per-user token management you MUST write
an Authenticator adapter (see Adding a new adapter)
that integrates with your own identity provider so each request carries
a verified, user-specific identity.
- Not an SSO/IdP. You must front it with your own identity layer.
- Not a credential distributor. The portal mints tokens but does NOT
install or sync them on developer machines; each developer copies the
freshly minted token into their own tool config (
.npmrc, CI secret, etc.) themselves. Skipping that automated push is the whole point: the portal exists for setups where MDM-style forced rollout of a shared bot credential is undesirable or infeasible. - Not a replacement for the bot credentials. The bot is still the organisation's privileged identity for Guard token issuance. The portal exists so the bot credentials only need to live in one server-side place, not on every developer or every CI worker.
- Not a token lifecycle manager. The portal mints (and optionally lets the caller revoke their own); everything else lives on the Guard side.
MIT, Copyright © 2026 GMO Flatt Security, Inc.

