Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Authentication

These endpoints really check credentials, so they work both as an upstream that a gateway must authenticate to (credential injection) and as a target behind a gateway that authenticates the client (then the gateway forwards or strips the credential and you can show the difference). Successful checks return {"authenticated": true, "auth_type": ..., ...}; failures return 401 with {"authenticated": false, "error": ...}. All of them accept any method and negotiate JSON or XML.

Basic auth

RouteExpected credentials
/auth/basic-authuser basic, password password
/auth/basic-auth/{username}/{password}the ones in the path

Failures carry WWW-Authenticate: Basic realm="rustybin", charset="UTF-8", so a browser shows its login dialog. Comparisons are constant time.

GET {{base_url}}/auth/basic-auth
[BasicAuth]
basic: password
HTTP 200
[Asserts]
jsonpath "$.authenticated" == true
jsonpath "$.auth_type" == "basic-auth"
jsonpath "$.username" == "basic"
POST {{base_url}}/auth/basic-auth/alice/s3cret
Authorization: Basic YWxpY2U6czNjcmV0
HTTP 200
[Asserts]
jsonpath "$.username" == "alice"
GET {{base_url}}/auth/basic-auth
HTTP 401
[Asserts]
header "WWW-Authenticate" == "Basic realm=\"rustybin\", charset=\"UTF-8\""
jsonpath "$.authenticated" == false

API keys

RouteExpected header
/auth/api-keyapikey: my-key
/auth/api-key/{header_name}/{key_value}the header and value in the path (header name case-insensitive)
GET {{base_url}}/auth/api-key
apikey: my-key
HTTP 200
[Asserts]
jsonpath "$.auth_type" == "api-key"
jsonpath "$.header" == "apikey"
GET {{base_url}}/auth/api-key/X-Token/s3cret
x-token: s3cret
HTTP 200

GET {{base_url}}/auth/api-key/X-Token/s3cret
X-Token: wrong
HTTP 401

JWT

RouteBehaviour
/auth/jwtValidates the Bearer token: signature, exp (required), nbf, with 30 s leeway, plus ?iss= and ?aud= when given
/auth/jwt/decodeDecodes the Bearer token without any validation: "what did the gateway forward?"
/auth/jwt/exchangeValidates like /auth/jwt, then returns a new HS256 token with the same claims and a new iss (rustybin), iat, jti and one hour exp

The algorithm comes from the token header and only two are accepted:

  • HS256 with the public demo secret rustybin-demo-secret-do-not-use-in-production (configure it in a gateway to mint or validate tokens Rustybin accepts);
  • RS256 with the key of the built-in identity provider, published at /oauth/jwks (kid rustybin-rs256-key). The key pair is generated at startup, so RS256 tokens do not survive a restart.

alg: none and every other algorithm are rejected. Failures carry an RFC 6750 challenge: WWW-Authenticate: Bearer realm="rustybin", error="invalid_token", error_description="...".

A ready-made HS256 demo token (sub 1234567890, name John Doe, iss and aud rustybin, scope "openid profile", valid until 2100):

# The demo HS256 token (secret rustybin-demo-secret-do-not-use-in-production, exp in 2100).
GET {{base_url}}/auth/jwt?iss=rustybin&aud=rustybin
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaXNzIjoicnVzdHliaW4iLCJhdWQiOiJydXN0eWJpbiIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6NDEwMjQ0NDgwMH0.o8obKOeREkQXJtz3ZyqxTs5bvJBn9lxiYnL2NC9Npbw
HTTP 200
[Asserts]
jsonpath "$.auth_type" == "jwt"
jsonpath "$.username" == "1234567890"
jsonpath "$.jwt_header.alg" == "HS256"
jsonpath "$.claims.name" == "John Doe"
GET {{base_url}}/auth/jwt?aud=payments-api
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaXNzIjoicnVzdHliaW4iLCJhdWQiOiJydXN0eWJpbiIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6NDEwMjQ0NDgwMH0.o8obKOeREkQXJtz3ZyqxTs5bvJBn9lxiYnL2NC9Npbw
HTTP 401
[Asserts]
header "WWW-Authenticate" contains "error=\"invalid_token\""
# RS256 tokens from the built-in identity provider validate too.
POST {{base_url}}/oauth/token
[Form]
grant_type: client_credentials
client_id: rustybin
client_secret: secret
HTTP 200
[Captures]
rs_token: jsonpath "$.access_token"

GET {{base_url}}/auth/jwt
Authorization: Bearer {{rs_token}}
HTTP 200
[Asserts]
jsonpath "$.jwt_header.alg" == "RS256"
jsonpath "$.jwt_header.kid" == "rustybin-rs256-key"
jsonpath "$.claims.client_id" == "rustybin"
# Decode WITHOUT validation: what did the gateway forward?
GET {{base_url}}/auth/jwt/decode
Authorization: Bearer eyJhbGciOiJub25lIn0.eyJzdWIiOiJnYXRld2F5LXVzZXIiLCJyb2xlIjoiYWRtaW4ifQ.
HTTP 200
[Asserts]
jsonpath "$.verified" == false
jsonpath "$.jwt_header.alg" == "none"
jsonpath "$.claims.sub" == "gateway-user"
GET {{base_url}}/auth/jwt
Authorization: Bearer eyJhbGciOiJub25lIn0.eyJzdWIiOiJnYXRld2F5LXVzZXIiLCJyb2xlIjoiYWRtaW4ifQ.
HTTP 401
POST {{base_url}}/auth/jwt/exchange
Authorization: Bearer {{rs_token}}
HTTP 200
[Captures]
exchanged: jsonpath "$.exchanged_token"
[Asserts]
jsonpath "$.claims.iss" == "rustybin"
jsonpath "$.claims.sub" == "rustybin"

GET {{base_url}}/auth/jwt/decode
Authorization: Bearer {{exchanged}}
HTTP 200
[Asserts]
jsonpath "$.jwt_header.alg" == "HS256"

HMAC signatures

/auth/hmac (user alice, secret secret) and /auth/hmac/{username}/{secret} validate request signatures in the common gateway hmac-auth format and in the draft-cavage HTTP Signatures format:

Authorization: hmac username="alice", algorithm="hmac-sha256", headers="date request-line", signature="<base64>"
Authorization: Signature keyId="alice", algorithm="hmac-sha256", headers="date (request-target)", signature="<base64>"
  • The credentials can also come in Proxy-Authorization.
  • Algorithms: hmac-sha1, hmac-sha256, hmac-sha384, hmac-sha512.
  • The signing string has one name: value line per header listed in headers (default date; repeated headers joined with , ), joined by \n. Two pseudo headers exist: request-line (GET /path?query HTTP/1.1) and (request-target) ((request-target): get /path?query).
  • Date or X-Date is required and must be within ?clock_skew= seconds of the server clock (default 300, at most 604800; 0 disables the check, handy for fixed examples).
  • When digest is signed, the Digest: SHA-256=<base64> (or SHA-512=) header is checked against the body.
  • The response shows the signing string the server computed, which is the fastest way to debug a gateway's signing plugin.
# Signing string: "date: <Date>\nGET /auth/hmac?clock_skew=0 HTTP/1.1",
# HMAC-SHA256 with the default secret `secret`, base64 encoded.
# clock_skew=0 disables the Date freshness check so the example stays valid.
GET {{base_url}}/auth/hmac?clock_skew=0
Date: Mon, 02 Jan 2006 15:04:05 GMT
Authorization: hmac username="alice", algorithm="hmac-sha256", headers="date request-line", signature="hQpFaCiOTydp6g+6wF5Un7Z+m/CWhnU2x7pOC9ofD+Y="
HTTP 200
[Asserts]
jsonpath "$.auth_type" == "hmac"
jsonpath "$.username" == "alice"
jsonpath "$.claims.algorithm" == "hmac-sha256"
jsonpath "$.claims.signed_headers" == "date request-line"
# draft-cavage style, HMAC-SHA512, with a body digest and path credentials (bob / s3cr3t).
POST {{base_url}}/auth/hmac/bob/s3cr3t?clock_skew=0
Date: Mon, 02 Jan 2006 15:04:05 GMT
Digest: SHA-256=8m4mfuAzMf9c4QtoehuhqbSQEv+ydpTJIuF0EbS4bmw=
Authorization: Signature keyId="bob", algorithm="hmac-sha512", headers="date (request-target) digest", signature="E1ntlIp2Br+QVxBf758Fn/lvg1Z+ko8cRo6HKRnBIj9rn0w3Z8StPrcZ440Kip3QMq44GYmMvclWhILfpiC2bw=="
Content-Type: application/json
`{"amount":42}`
HTTP 200
[Asserts]
jsonpath "$.claims.digest_verified" == true
GET {{base_url}}/auth/hmac
HTTP 401
[Asserts]
header "WWW-Authenticate" contains "hmac realm=\"rustybin\""

mTLS

/auth/mtls authenticates a client certificate issued by the demo CA (see the demo PKI). It looks in two places:

  1. TLS mode: the certificate the client presented to Rustybin's own HTTPS listener (verified during the handshake).
  2. Header mode: when RUSTYBIN_MTLS_IN_HEADER names a header, a gateway that terminated mTLS can forward the client certificate there (URL-encoded PEM, raw PEM with spaces instead of newlines, or base64 DER). Rustybin verifies the signature chain to the demo CA and the validity period.

The response has the subject (client_dn), issuer (client_ca) and, in claims, serial, validity, SHA-256 fingerprint and source (tls or header).

RoutePurpose
GET /auth/mtls/get-client-certA client certificate and key issued by the demo CA (cert_pem, key_pem)
GET /auth/mtls/get-ca-certThe demo CA certificate (ca_cert_pem), to verify the HTTPS listener or configure a gateway's trust store
GET {{base_url}}/auth/mtls/get-client-cert
HTTP 200
[Captures]
client_cert: jsonpath "$.cert_pem" urlEncode
[Asserts]
jsonpath "$.cert_pem" startsWith "-----BEGIN CERTIFICATE-----"
jsonpath "$.key_pem" contains "PRIVATE KEY"
GET {{base_url}}/auth/mtls/get-ca-cert
HTTP 200
[Asserts]
jsonpath "$.ca_cert_pem" startsWith "-----BEGIN CERTIFICATE-----"

Header mode (the documentation examples run with RUSTYBIN_MTLS_IN_HEADER=X-Client-Cert; Hurl's urlEncode filter URL-encodes the PEM):

GET {{base_url}}/auth/mtls
X-Client-Cert: {{client_cert}}
HTTP 200
[Asserts]
jsonpath "$.auth_type" == "mtls"
jsonpath "$.header" == "X-Client-Cert"
jsonpath "$.client_dn" contains "CN="
jsonpath "$.claims.source" == "header"
jsonpath "$.claims.sha256_fingerprint" exists
GET {{base_url}}/auth/mtls
HTTP 401
[Asserts]
jsonpath "$.error" contains "X-Client-Cert"

TLS mode with curl ($HTTP and $HTTPS are the HTTP and HTTPS base URLs, for example http://localhost:8080 and https://localhost:8443):

curl -s "$HTTP/auth/mtls/get-client-cert" | jq -r .cert_pem > client.crt
curl -s "$HTTP/auth/mtls/get-client-cert" | jq -r .key_pem > client.key
curl -s "$HTTP/auth/mtls/get-ca-cert" | jq -r .ca_cert_pem > ca.crt
# The server certificate is issued for localhost / 127.0.0.1 by the demo CA.
curl -s --cacert ca.crt --cert client.crt --key client.key "$HTTPS/auth/mtls"
# Without a client certificate the TLS handshake still succeeds (client
# certificates are optional); the header fallback then reports what is missing.
curl -s --cacert ca.crt "$HTTPS/auth/mtls"

Behind a platform that terminates TLS itself (Fly.io's edge, most load balancers), the HTTPS listener is not reachable from clients: use header mode.

OAuth 2.0 and OpenID Connect

Tokens for /auth/jwt, the MCP and A2A demos and your gateway's OIDC plugin come from the built-in identity provider: see OAuth 2.0 / OIDC provider.