Docs

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.