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:
| Request | Result |
|---|---|
No Authorization header | 401, with WWW-Authenticate: Bearer realm="petstore" |
Authorization: Bearer petstore-user | 200, principal alice |
Authorization: Bearer petstore-admin | 200, principal admin |
| Any other token | 401 |
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-Tokenheader. 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, andpetstore.jssends it asX-Csrf-Token. This token is not the console'sdata-juneau-csrftoken, 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
Originis admitted when it carries a JSON, XML or UON body, or usesPUT,DELETEorPATCH. A urlencoded, multipart ortext/plainPOST with noOriginis refused withSIMPLE_REQUEST_WITHOUT_ORIGIN:curl -H 'Content-Type: application/json' -d '{...}' http://localhost:10000/petstore/pets
curl -d a=b http://localhost:10000/petstore/petsThe 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=DELETEis treated as a write. Past the filter, the router refuses it too: an override of aGETto a write is answered405. -
Refusals name their reason in the
X-Request-Boundaryresponse 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 otherHostis answered with 421HOST_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 setpetstore.admin.allowedHoststhe same way to the names you reach them under. - Behind a TLS-terminating proxy. The boundary compares the browser's
httpsorigin with thehttprequest 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'sCONTAINERorigin source. The petstore does not ship that switch. - The console.
petstore.console.authorityis 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.