Skip to main content

Petstore: Secure

The Secure page (/console/dev/secure) pairs an open console page with a bearer-token protected API underneath it. Its "Try it" card calls the API from your browser with no token, with a demo token and with a wrong token, and prints the status and body, so you see the 401 and the success side by side. The page also marks the line between two different protections: the bearer guard on the API, and the CSRF boundary that guards writes to the console itself. This page covers both, adds the separate CSRF boundary on the /petstore API, and shows how the Jetty and Spring Boot runners install the boundaries.

The bearer-token demo​

The page is open. The API under /console/dev/secure/api is SecureApiRest, and every operation on it is gated by a fail-closed BearerTokenGuard with the realm petstore, backed by StubBearerTokenValidator:

RequestResult
No Authorization header401, with WWW-Authenticate: Bearer realm="petstore"
Authorization: Bearer petstore-user200, principal alice
Authorization: Bearer petstore-admin200, principal admin
Any other token401

The API has GET /pets, GET /pets/{id} (404 for an unknown pet) and GET /whoami, which returns the caller's name through an @Auth Principal parameter. The guard is one @Bean returning a RestGuardList, so it covers every operation on the resource. The siblings stay open: the guard does not touch /petstore or the other console pages. From a shell:

curl -H 'Authorization: Bearer petstore-user' http://localhost:10000/console/dev/secure/api/pets

With a single mechanism, the op-level guard is the whole story. AuthFilterChain is for composing several mechanisms; it is fail-open by design, so a downstream guard still decides.

The CSRF boundary​

Everything under /console is also behind PetstoreConsoleBoundary, a servlet Filter over the framework's LoopbackBoundaryFilter. It pins the Host header on every console request, and on console writes it also requires a same-origin Origin, a JSON body and the per-process synchronizer token, which the page renderer stamps on <body data-juneau-csrf>. The checks are listed in Security Model.

The boundary judges the method a request claims, not only its request line. A method query parameter, or an X-Method, X-HTTP-Method-Override, X-HTTP-Method or X-Method-Override header naming a non-safe method, makes the request a write. Behind a Docker port mapping or a reverse proxy, set the petstore.console.authority setting to the host and port the browser uses, or the pinned Host check answers 421.

The /petstore-ui app and the Jetty admin resources are outside this boundary. The /petstore API has a boundary of its own, described next, and so do the admin resources.

The API's CSRF boundary​

Everything under /petstore is behind PetstoreApiBoundary, a servlet Filter over the framework's CsrfBoundary (through a RequestBoundaryFilter). This is separate from the bearer guard above, which covers only /console/dev/secure/api; the petstore API has no bearer guard. The framework's checks are described in CSRF protection: loopback vs general, and the console side in Security Model.

  • Browser writes. A write from a browser must come from the page's own origin and carry the API's token in the X-Csrf-Token header. The token travels only in that header, so an HTML <form> POST to the API is refused, the application's own forms included.

  • The console's Photo tab. It is the console's one write to the API. The console boundary hands the page an API token, the page stamps it on the upload form as data-petstore-csrf, and petstore.js sends it as X-Csrf-Token. This token is not the console's data-juneau-csrf token, and each boundary refuses the other's.

  • The token is per process. A restart invalidates it, so reload any open page after restarting the server.

  • Command-line and program clients need nothing special, provided the request is not a simple form-type request. A request with no Origin is admitted when it carries a JSON, XML or UON body, or uses PUT, DELETE or PATCH. A urlencoded, multipart or text/plain POST with no Origin is refused with SIMPLE_REQUEST_WITHOUT_ORIGIN:

    curl -H 'Content-Type: application/json' -d '{...}' http://localhost:10000/petstore/pets
    curl -d a=b http://localhost:10000/petstore/pets

    The first is admitted; the second is answered with 403.

  • Method overrides count. Requests are judged on their effective method, so GET /petstore/pets/1?method=DELETE is treated as a write. Past the filter, the router refuses it too: an override of a GET to a write is answered 405.

  • Refusals name their reason in the X-Request-Boundary response header.

The /petstore-ui app stays outside: it only reads. The Jetty runner's admin resources (/config, /logs and /shutdown) are provided by the microservice, not by the petstore, and are behind their own boundary, AdminBoundary (see Protecting the admin resources). It answers to the host names in its own setting, petstore.admin.allowedHosts; while that is unset, only localhost, 127.0.0.1 and [::1] are answered, and any other Host gets 421 HOST_NOT_ALLOWED, reads included. petstore.api.allowedHosts does not apply to it. The admin resources have no forms, so every browser write to them is refused; curl -X POST -H 'Content-Type: application/json' http://localhost:10000/shutdown and other PUT, DELETE or JSON POST requests from the command line are admitted. The boundary does not authenticate, and the admin resources are not read-only (/shutdown is not), so do not expose them on an untrusted network.

Deploying on a real host​

  • Loopback development. Nothing needs to be set.
  • A plain-HTTP host. Set petstore.api.allowedHosts (a system property, an environment setting or any registered setting source) to the comma-separated host names the browser uses, without ports. Any other Host is answered with 421 HOST_NOT_ALLOWED; this is the DNS-rebinding defence. It is read once at startup, and a malformed entry fails startup. The admin resources stay loopback-only unless you also set petstore.admin.allowedHosts the same way to the names you reach them under.
  • Behind a TLS-terminating proxy. The boundary compares the browser's https origin with the http request the container sees, and it never reads forwarded headers, so writes are refused. Such a deployment needs the container's forwarded-header support together with the framework boundary's CONTAINER origin source. The petstore does not ship that switch.
  • The console. petstore.console.authority is unchanged and still applies to the console boundary.

Installing the boundaries​

The console boundary covers PetstoreConsoleBoundary.URL_PATTERN, which is /console/*. The API boundary covers PetstoreApiBoundary.URL_PATTERN, which is /petstore/*. The two patterns never overlap.

Each runner builds one PetstoreApiBoundary and gives that same instance to both registrations, because the console boundary mints the API token its pages carry and a token from any other instance is refused.

Spring Boot. App declares a FilterRegistrationBean for the boundary on that pattern with the highest precedence, for the REQUEST, ASYNC, ERROR and FORWARD dispatcher types, so a dispatch that lands on a console path is checked too.

Jetty. App.AppConfig in juneau-petstore-jetty contributes a ServletFilterRegistration bean for the boundary on that pattern with HIGHEST_PRECEDENCE, for the same four dispatcher types. The server component mounts it as a container filter before the servlets, ahead of authentication, so the Jetty runner and the Spring Boot runner behave alike. INCLUDE dispatches are deliberately not mapped. See Registering servlet filters for the registration API.

The API registration. On Spring Boot, a second FilterRegistrationBean named apiBoundary covers PetstoreApiBoundary.URL_PATTERN with Ordered.HIGHEST_PRECEDENCE + 10. On Jetty, App.AppConfig has an apiBoundary @Bean returning a ServletFilterRegistration for the same pattern with ServletFilterRegistration.HIGHEST_PRECEDENCE + 10. Both are registered for the REQUEST dispatcher type only, so an internal forward or error dispatch to the API is not checked again. The order puts the API boundary just after the console boundary; the petstore mounts no authentication filter between them.

Template​

The page itself is plain cards; the buttons are wired by secure.js, which init loads:

<@page tab="dev/secure" init=["/console/dev/secure/secure.js"]>
<@card id="try" title="Try it">
<button type="button" data-secure-call="none" data-secure-url="/console/dev/secure/api/whoami">Without a token</button>
<button type="button" data-secure-call="petstore-user" data-secure-url="/console/dev/secure/api/whoami">With the demo token</button>
<pre><output id="secure-result" aria-live="polite"></output></pre>
</@card>

Builder twin​

return PageSpec.create().tab("dev/secure").init("/console/dev/secure/secure.js")
.html("about", "Bearer-token guard", "<div class=\"jc-prose\">...</div>")
.html("try", "Try it", "<div class=\"jc-prose\">...<button data-secure-call=\"none\" ...>...</div>");

Source: SecureRest, SecureApiRest and secure.js in juneau-petstore/juneau-petstore-core/src/main/java/org/apache/juneau/petstore/console/dev/; PetstoreConsoleBoundary and PetstoreApiBoundary in .../console/; StubBearerTokenValidator in .../petstore/auth/; DevPageBuilders.secure in .../console/pages/; secure.ftlh under .../console/templates/. The runners are App and its nested App.AppConfig in juneau-petstore/juneau-petstore-jetty and App in juneau-petstore/juneau-petstore-springboot.

Tests: SecureRest_Test, PetstoreConsoleBoundary_Test, PetstoreApiBoundary_Test, PetstoreSecure_BrowserTest, and the console cases in PetstoreJetty_Test and PetstoreSpringboot_Test.

See also​