Auth
HTTP primitives live in core. JWT verification lives in heddle-oauth. Missing credentials from
Auth.* are 401 with WWW-Authenticate.
On the example hub, writes and MCP HTTP share one bearer. Swagger Authorize and POST /mcp are
not two security stories.
Basic, Bearer, API key
Middleware.basicAuth is the lock. Auth.bearer is the extractor you provided onto routes
that need a subject. API keys are Auth.apiKey / Middleware.apiKey.
{
val locked =
Routes(Method.GET / "secret" -> Handler.text("ok")) @@ Middleware.basicAuth("ada", "pw")
val authed =
Request.get("/secret").copy(headers = Headers.empty.set(Authorization.basic("ada", "pw")))
for
denied <- locked(Request.get("/secret"))
ok <- locked(authed)
yield (denied.status, ok.status, ok.body.asString)
}(Status(401,Unauthorized),Status(200,OK),ok)OAuth / OIDC
heddle-oauth signs and verifies compact JWTs (RS256, our Jose), fetches JWKS, and speaks
authorization-code+PKCE, client credentials, refresh, device, and userinfo.
A loopback OpenID provider is sbt oauth/run
(http://127.0.0.1:8080/.well-known/openid-configuration).
sbt example/run embeds that OP next to the box office API so Swagger Authorize works against the
same process. Seed user ada / ada. Machine client machine / secret.
{
val key = SigningKey.generateRsa("k1")
val issuer = "http://iss"
val aud = "api"
val token = Jose.sign(key, "ada", issuer, aud, Set("openid"), 5.minutes)
JwtVerifier.static(key.publicJwksJson, issuer, aud).flatMap(_.verify(token)).map { claim =>
(claim.subject, claim.scopes.contains("openid"))
}
}(ada,true)Endpoint.auth(SecurityScheme.OAuth2(...)) or HttpBearer is what Swagger uses for Authorize.
Do not put a second authorization server inside heddle-mcp. Resource-server JWT verify is the
adapter MCP HTTP already expects.