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
| Route | Expected credentials |
|---|---|
/auth/basic-auth | user 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
| Route | Expected header |
|---|---|
/auth/api-key | apikey: 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
| Route | Behaviour |
|---|---|
/auth/jwt | Validates the Bearer token: signature, exp (required), nbf, with 30 s leeway, plus ?iss= and ?aud= when given |
/auth/jwt/decode | Decodes the Bearer token without any validation: "what did the gateway forward?" |
/auth/jwt/exchange | Validates 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:
HS256with the public demo secretrustybin-demo-secret-do-not-use-in-production(configure it in a gateway to mint or validate tokens Rustybin accepts);RS256with the key of the built-in identity provider, published at/oauth/jwks(kidrustybin-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: valueline per header listed inheaders(defaultdate; 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). DateorX-Dateis required and must be within?clock_skew=seconds of the server clock (default 300, at most 604800;0disables the check, handy for fixed examples).- When
digestis signed, theDigest: SHA-256=<base64>(orSHA-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:
- TLS mode: the certificate the client presented to Rustybin's own HTTPS listener (verified during the handshake).
- Header mode: when
RUSTYBIN_MTLS_IN_HEADERnames 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).
| Route | Purpose |
|---|---|
GET /auth/mtls/get-client-cert | A client certificate and key issued by the demo CA (cert_pem, key_pem) |
GET /auth/mtls/get-ca-cert | The 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.