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)
    body   <- ok.body.utf8
  yield (denied.status, ok.status, body)
}
(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.

Signing and verifying are effects on the Rsa service (Rsa.live is the platform's own RSA: the JCA, Node crypto, or OpenSSL). They read Clock for iat and exp and the CSPRNG for jti, so a TestClock test can expire a token without waiting. A verifier checks the issuer and requires an audience; a token whose header names no kid verifies only against a one-key set. A remote verifier fetches its JWKS before its layer is built, refreshes every five minutes, and refetches for an unknown kid at most once every 30 seconds, so forged kids do not become outbound requests. Failures are typed: JoseError inside OAuthError.InvalidToken, RsaError inside OAuthError.Crypto.

Stored passwords are salted PBKDF2-HMAC-SHA256 at 600,000 iterations, checked in constant time.

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.

(for
  key   <- SigningKey.generateRsa("k1")
  token <- Jose.sign(key, TokenClaims("ada", "http://iss", "api", Set("openid"), 5.minutes))
  claim <- JwtVerifier.static(key.publicJwksJson, "http://iss", "api").flatMap(_.verify(token))
yield (claim.subject, claim.scopes.contains("openid"))).provide(Rsa.live)
(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.