Skip to content

flatt-security/takumi-guard-portal-example

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

guard-token-portal

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.

Experience example

What the portal looks like in production, deployed behind Google IAP for a single Google Workspace user:

Manage view (bot has list/revoke permissions)

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.

Manage view: the caller's tokens, with mint button and per-row revoke

After minting a token

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.

Token minted: the plaintext token shown once, plus token id / prefix / user_identifier / org / created-at metadata

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.

Design at a glance

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
Loading

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.

Capability-driven UI

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
  • POST /o/{orgID}/guard/org-user-tokens
issue_takumi_guard_token + list/revoke Mint button + list of caller's own tokens with per-row revoke
  • POST /o/{orgID}/guard/org-user-tokens
  • GET /o/{orgID}/guard/org-user-tokens?user_identifier_prefix=… (filtered)
  • DELETE /o/{orgID}/guard/org-user-tokens/by-id/{token_id}

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.

Self-only, double-filtered

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:

  1. Server-side prefix filter. The portal sends the caller's email as user_identifier to Guard's list endpoint. Guard treats this as a prefix match (so the wire result set is bounded by O(your tokens), not O(org tokens)).
  2. Client-side exact match. The portal then drops any returned row whose user_identifier is not byte-for-byte equal to the caller's email. This is the defence that closes the prefix-match gap: a row with user_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.

How to use

1. Create a Shisho Cloud bot

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.

2. Stand up the portal

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).

3. Hand the URL to your developers

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).

Zero third-party dependencies

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.

Authenticator adapters

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

Adding a new adapter

  1. Write a sibling file in internal/auth/ (e.g. oidc.go) defining a type that implements Authenticator and a private constructor returning (Authenticator, error). Read any env vars the adapter needs inside that constructor; keep build.go dumb.
  2. Add a case "yourmode": to the switch in build.go.
  3. 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.

Configuration

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.

Build & run (locally)

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-portal

Then open http://localhost:8080 and sign in. The username you type is the user_identifier that Guard will record on every token you mint.

Deployment examples

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.

Security notes

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.

What this portal is NOT

  • 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.

License

MIT, Copyright © 2026 GMO Flatt Security, Inc.

About

An example implementation for a Takumi Guard portal to control org user tokens

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Contributors