Identity and the phantom token
The pattern this arrangement implements is the phantom token. A browser holds an opaque session cookie and never sees a token. The gateway exchanges that cookie for a real token by asking the identity server, and passes the token inward. The api therefore receives a normal bearer token and knows nothing about cookies.
It takes three components: the identity server, its database, and the gateway that does the exchange.
The identity server
Curity is somebody else's software, so the component declares the interfaces it serves and a variant names the image that runs it:
kind = "online-service"
subdomain = "auth"
links.db = "postgres/db"
use = { cluster = "hosted", _default = "main" }
[interfaces.oauth]
kind = "other"
base-path = "/oauth"
public = true
[interfaces.oauth.profiles.introspect]
credential-bundle = "introspection"
use picks the variant per environment. In cluster it resolves to
hosted.toml, whose kind = "external" means nothing is deployed and the
serving coordinates are somebody's tenant; everywhere else it resolves to
main.toml, a container we run. Consumers are untouched by the choice, because
they name the component and its interface, never a deployment.
public = true marks an interface a browser reaches directly, which is what
a login page has to be. Interfaces without it are reachable only inside the
deploy.
Curity stores accounts, so it links a database — and that database is a component of this system like any other, with its own image, credentials and manifests.
The gateway
routes.api = { is = "api/todos", headers = { "token-handler-version" = "1" } }
routes.apps = "curity/apps"
routes.userinfo = "curity/userinfo"
links.introspect = "curity/introspect"
routes are what the gateway forwards; links are what it calls. The exchange
is the link: the gateway takes the cookie, calls Curity's introspection
endpoint, and passes the resulting token inward. The headers clause adds the
token-handler protocol's required header to every forwarded api call, declared
here rather than buried in Kong's own configuration file.
The credentials the exchange needs are a credential bundle:
mounts = ["cookie"]
[credential-bundles.cookie]
secret-keys = ["key", "key-pass"]
A bundle declares which secret keys exist by name; mounts says this component
receives them as files. No value is in the repository. You write them once with
b secret set <config> <address>, reading the value from standard input, and
the deploy places them for the components that declare them.
What the api declares
Two lines, at the top of its endpoint file:
caller = { admit = "admit_anyone" }
auth-refusal = "todos-auth-refusal-wire"
admit names the rule that decides who may call the surface, and it applies to
every endpoint in it, so tightening one line tightens all of them.
auth-refusal is the wire shape returned when the rule refuses.
The api receives an ordinary bearer token. It never parses a cookie, never calls the identity server, and has no declaration mentioning the gateway — which is why the same api serves the command-line client, where there is no browser and no cookie at all.