Class PetstoreApiBoundary

java.lang.Object
org.apache.juneau.petstore.console.PetstoreApiBoundary
All Implemented Interfaces:
jakarta.servlet.Filter

public class PetstoreApiBoundary extends Object implements jakarta.servlet.Filter
The /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 ServletFilterRegistration @Bean just after the console boundary's, sharing the one instance with it:

private final PetstoreApiBoundary apiBoundary = new PetstoreApiBoundary(); @Bean public ServletFilterRegistration apiBoundary() { return ServletFilterRegistration.create(apiBoundary) .urlPatterns(PetstoreApiBoundary.URL_PATTERN) .dispatcherTypes(DispatcherType.REQUEST) .order(ServletFilterRegistration.HIGHEST_PRECEDENCE + 10) .build(); }

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

    Fields
    Modifier and Type
    Field
    Description
    static final String
    Setting (system property, environment, or any registered source) listing the host names the API answers to: comma-separated, no ports.
    static final String
    The covered path: the API resource's mount point.
    static final String
    Request attribute under which an allowed console request carries a String: a freshly masked token for this boundary, for a console page to stamp on the form that writes to the API.
    static final String
    The servlet URL pattern the boundary is registered under.
  • Constructor Summary

    Constructors
    Constructor
    Description
    Constructor.
  • Method Summary

    Modifier and Type
    Method
    Description
    void
    doFilter(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, wait

    Methods inherited from interface jakarta.servlet.Filter

    destroy, init
  • Field Details

    • PATH

      public static final String PATH
      The covered path: the API resource's mount point.
      See Also:
    • URL_PATTERN

      public static final String URL_PATTERN
      The servlet URL pattern the boundary is registered under. It matches PATH itself and every path under it.
      See Also:
    • ALLOWED_HOSTS_PROPERTY

      public static final String 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

      public static final String TOKEN_ATTRIBUTE
      Request attribute under which an allowed console request carries a String: 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

  • Method Details

    • tokenFor

      public String tokenFor(jakarta.servlet.http.HttpServletRequest req)
      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 be null.
      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:
      doFilter in interface jakarta.servlet.Filter
      Throws:
      IOException
      jakarta.servlet.ServletException