Class PetstoreApiBoundary
- All Implemented Interfaces:
jakarta.servlet.Filter
/petstore API's CSRF boundary: a RequestBoundaryFilter over the framework's CsrfBoundary,
applied to /petstore and everything under it.
A browser write to the API must come from the page's own origin and carry this boundary's token in the
X-Csrf-Token header. The token travels in the header only, so every HTML <form> POST to
the API is refused, the application's own included. Requests are judged on their effective method, so
GET /petstore/pets/1?method=DELETE counts as a write. Reads are allowed, and so are non-browser writes
(no Origin and no Sec-Fetch-Site) that a browser could not have sent cross-site without a CORS
preflight: JSON, XML or UON bodies, and PUT, DELETE or PATCH. A non-browser urlencoded,
multipart or text/plain POST is refused. Every refusal names its reason in the
"X-Request-Boundary" response header.
The token
The token is per process (CsrfTokens.perProcess()): the petstore has no login session, and the
Origin check does the real work. A restart invalidates the token on every open page, so reload them. A
token that leaks through a page stays valid until the restart.
This token is not the console's. The console boundary (PetstoreConsoleBoundary) has its own, and each
boundary refuses the other's. The console's one write to the API, its photo upload, gets this boundary's token
from the console boundary, which publishes tokenFor(HttpServletRequest) under TOKEN_ATTRIBUTE
on every console request it allows; the page's script sends it in that header. So a runner must
give the console boundary the same instance it mounts here: a token from any other instance is refused.
The console page shell marks a token-bearing page no-store, and when the response is already committed it
strips only its own token attributes. So on the rare page whose buffer was flushed before the shell ran, this
boundary's token can still be emitted, possibly without no-store. That is accepted: this token is only
ever published on console pages, which carry the console's token too, so on every normal path the page is already
marked.
The host
The request's own origin is its scheme plus its Host header; forwarded headers are never read. By default
any Host is answered, which suits a loopback developer setup. On a host served over plain HTTP, set
"petstore.api.allowedHosts" (a system property, the environment, or any registered setting source) to the
comma-separated host names the browser uses, without ports. Every request to the API under any other Host,
reads included, is then answered with 421: this is the DNS-rebinding defence. The setting is read once,
when the boundary is constructed, and a malformed entry fails construction rather than being ignored.
Behind a TLS-terminating proxy the browser's https origin does not match the http request the
container sees, so writes are refused. Such a deployment needs the container's forwarded-header support and the
framework boundary's CONTAINER origin source, which this demo does not
configure.
Example:
In a Juneau microservice, contribute a
and build the console boundary with new PetstoreConsoleBoundary(apiBoundary). Register it for
REQUEST dispatches only: an internal forward or error dispatch is not a new request from the browser.
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final StringSetting (system property, environment, or any registered source) listing the host names the API answers to: comma-separated, no ports.static final StringThe covered path: the API resource's mount point.static final StringRequest attribute under which an allowed console request carries aString: a freshly masked token for this boundary, for a console page to stamp on the form that writes to the API.static final StringThe servlet URL pattern the boundary is registered under. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionvoiddoFilter(jakarta.servlet.ServletRequest req, jakarta.servlet.ServletResponse res, jakarta.servlet.FilterChain chain) Applies the boundary: a refused request is answered here and never reaches the API.tokenFor(jakarta.servlet.http.HttpServletRequest req) A freshly masked token for a page served to this request to embed.Methods inherited from class java.lang.Object
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, waitMethods inherited from interface jakarta.servlet.Filter
destroy, init
-
Field Details
-
PATH
The covered path: the API resource's mount point.- See Also:
-
URL_PATTERN
The servlet URL pattern the boundary is registered under. It matchesPATHitself and every path under it.- See Also:
-
ALLOWED_HOSTS_PROPERTY
Setting (system property, environment, or any registered source) listing the host names the API answers to: comma-separated, no ports. Unset or blank, any host is answered.- See Also:
-
TOKEN_ATTRIBUTE
Request attribute under which an allowed console request carries aString: a freshly masked token for this boundary, for a console page to stamp on the form that writes to the API.- See Also:
-
-
Constructor Details
-
PetstoreApiBoundary
public PetstoreApiBoundary()Constructor.Reads "petstore.api.allowedHosts" and creates the boundary's per-process token.
- Throws:
IllegalArgumentException- If "petstore.api.allowedHosts" holds an entry that is blank or not a bare host name.
-
-
Method Details
-
tokenFor
A freshly masked token for a page served to this request to embed.Never creates a session. The value is different on every call, and every value verifies on this instance only.
- Parameters:
req- The request whose response will embed the token. Must not benull .- Returns:
- 128 lower-case hex characters.
-
doFilter
public void doFilter(jakarta.servlet.ServletRequest req, jakarta.servlet.ServletResponse res, jakarta.servlet.FilterChain chain) throws IOException, jakarta.servlet.ServletException Applies the boundary: a refused request is answered here and never reaches the API.- Specified by:
doFilterin interfacejakarta.servlet.Filter- Throws:
IOExceptionjakarta.servlet.ServletException
-