Skip to content

Connect a console

Give a web console for people a login against the issuer. A service API that workloads call is a second listener: see service to service.

Before you start

  • Pick the shape first with choose native or gateway OIDC.
  • A console keeps its own hostname. Only the directory console shares the issuer hostname, under /console/ (console).
  • Never list your sign-out landing page in redirects. The render refuses an address in both redirects and signed_out.
Gateway OIDC Own flow
Login runs in The gateway SecurityPolicy, which forwards a verified bearer The console, which sends the browser to /authorize
Client kind confidential, the gateway holds the secret public, with PKCE
You deploy Your chart and a SecurityPolicy, or oauth2-proxy on another gateway Your chart

Steps

1. Declare the groups and the client

groups:
  all:myconsole:operator: { members: [[email protected]] }
  all:myconsole:viewer:   { members: [[email protected]] }
clients:
  myconsole.example.internal:
    kind: confidential          # public for your own flow, with no secret
    secret: myconsole-client
    pkce: required              # when the console sends an S256 challenge; a public client always does
    display_name: My Console    # shown on the sign-in page
    description: the team's dashboard
    redirects:  [https://myconsole.example.internal/oauth2/callback]
    signed_out: [https://myconsole.example.internal/]
    requires:   [all:myconsole:operator, all:myconsole:viewer]
    # backchannel_logout_uri: https://myconsole.example.internal/backchannel

The group name is the value in the token, so name it what the console checks. Nobody outside requires gets a token. display_name and description are shown to anyone who starts a sign-in. Only a console with its own flow can take backchannel_logout_uri. Group naming is in trust.

2. Verify tokens in the backend

issuer := &identity.Issuer{URL: "https://issuer.example.internal", Audience: "myconsole.example.internal"}

mux := http.NewServeMux()
mux.Handle(identity.WhoAmIPath, identity.WhoAmI(version))
mux.Handle("/admin/", identity.Require("all:myconsole:operator")(admin))

http.ListenAndServe(":8080", identity.Middleware(issuer)(mux))
import express from "express";
import { Issuer, middleware, requireGroups, whoami, whoamiPath } from "@truvity/sluis/server";

const issuer = new Issuer({ url: "https://issuer.example.internal", audience: "myconsole.example.internal" });

const app = express();
app.use(middleware(issuer));
app.get(whoamiPath, whoami(version));
app.use("/admin", requireGroups("all:myconsole:operator"), admin);

Middleware establishes the caller and Require refuses, so health and landing routes run before anyone is established. The caller carries name, givenName, familyName, the address and the groups, so there is no userinfo call. Both verifiers follow the key the issuer advertises, so set no signing algorithm (configuration).

3. Use the frontend package

Use useIdentity() and <UserBadge/> from @truvity/sluis (install). The console writes no login page, session, token parsing or sign-out logic.

4. Attach the route

If your platform publishes listeners as a ListenerSet, write parentRefs in full: group, kind, name, namespace. Point DNS for the hostname at the gateway.

Traps

  • Long work inside a request dies at the gateway route timeout. Trigger it and poll.
  • Test authorization at the handler. An unwired role check still passes its unit test.
  • Never mount an operator RPC on the service API port.
  • No authz package exists. Pass Require the group names as the policy spells them (Go module).

Verify

Open the console signed out and expect a redirect to the issuer. Sign in as a viewer and expect identity.WhoAmIPath to answer with your identity.

Decided in

ADR 0001, ADR 0003.