Microprofile JWT Auth and Cookie?
Microprofile JWT Auth is a nice specification allowing to enforce authentication and potentially authorizations based on JWT tokens. It works well for machine to machine communication or for Single Page Applications (SPA) where the application is managed by code but for server side web applications it is less integrated.
To give you an example, if you write a website with a login button on the top right, you will desire to see the username and a "logout" button instead of "login" once you logged in. Sounds natural right? With Microprofile JWT Auth, it requires to use the Authorization HTTP header and to have "Bearer <token>" in it. However, the best way to manage the login in a web application managed by the server is to use a Cookie. This enables to enforce the cookie to be sent by the client for any communication of the web application and potentially even forbids the javascript to access this cookie. This is not something you can do for headers today if you don't code your full application in javascript.
So at the end we have a security mecanism based on a Cookie and a security layer based on an header....it will not work well.
Now it is not that bad because JWT Auth implementation is based on JAX-RS and therefore you can add a server filter redirecting the cookie - if exists - to the header. It will let JWT Auth implementation to read the cookie as a header and behave properly.
There are two kind of implementations for JWT Auth:
- Fully JAX-RS based, this is nice because you just need to add a ContainerRequestFilter doing the forwarding,
- Servlet based implementations. Here you need to add a servlet Filter doing the forwarding and ensure it is before the JWT Auth one.
Side note: the implementation can use both but using servlet based one enables to integrate with servlets as well as JAX-RS endpoints which is more generic and works really smoothly with libraries whereas a fully JAX-RS based implementation will requires some custom code to integrate with most libraries.
JAX-RS filter redirecting a cookie to a header
Based on JAX-RS the implementation is quite trivial:
- Check the header is already there, if so do nothing,
- If the header is not set, check there is the cookie, if so get its value and set it as header.
Here is a potential implementation:
@Provider
@Dependent
@PreMatching // 1
@Priority(Priorities.AUTHENTICATION - 10) // 2
public class ForwardingContainerRequestFilter implements ContainerRequestFilter {
@Inject // 2
@ConfigProperty(name = "app.security.cookieName", defaultValue = "Bearer")
private String cookieName;
@Override
public void filter(final ContainerRequestContext requestContext) {
if (requestContext.getHeaderString(HttpHeaders.AUTHORIZATION) != null) { // 3
return;
}
final Cookie bearer = requestContext.getCookies().get(cookieName");
if (bearer != null) { // 4
requestContext.getHeaders().putSingle(HttpHeaders.AUTHORIZATION, "Bearer " + bearer.getValue());
}
}
}
- We ensure we are prematching, which means we are executed before any resource matching in JAX-RS router,
- We ensure we are executed before authentication filters - if they are pre-matching as well otherwise it is useless,
- If the header is set we don't change it,
- If the cookie is set and there is no header then we forward its value in headers before the authentication filter reads it.
This implementation is simple but can not work if the JWT Auth implementation is based on Servlet layer instead of JAX-RS which is more generic than JAX-RS which is quite late in servlet filter (it is a servlet so you passed all fitlers without security activated if the implementation uses that). Let see how to do the same based on servlet layer - which should also work for JAX-RS based implementation if they didn't bypass Servlet layer.
Servlet filter redirecting a cookie to a header
For servlet case the filter will look like this:
// @WebFilter(urlPatterns = "/*", asyncSupported = true) // 1
public class CookieToHeaderFilter implements Filter {
@Inject // 2
@ConfigProperty(name = "app.security.cookieName", defaultValue = "Bearer")
private String cookieName;
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
// 3
chain.doFilter(getRequest(HttpServletRequest.class.cast(request)), response);
}
// forward cookie to header to match geronimo-jwt-auth impl
private HttpServletRequest getRequest(final HttpServletRequest httpServletRequest) {
return ofNullable(httpServletRequest.getCookies()).map(Stream::of).orElseGet(Stream::empty) // 4
.filter(it -> cookieName.equals(it.getName())) // 5
.findFirst()
.map(cookie -> HttpServletRequest.class.cast(new HttpServletRequestWrapper(httpServletRequest) {
@Override
public String getHeader(final String name) {
if (HttpHeaders.AUTHORIZATION.equalsIgnoreCase(name)) { // 6
return "Bearer " + cookie.getValue();
}
return super.getHeader(name);
}
}))
.orElse(httpServletRequest);
}
}
- We bind the filter on the whole application (/*) - you can also limit that to a subcontext if needed, and we ensure we don't break asynchronous filter/servlets after this one. Generally this filter should be very early in the filter chain so asynchronism does not affect it since it does not use any thread local storage. Note that this line is commented because of the ordering point, see later,
- We inject the cookie name used for the security since applications can customize it to avoid conflicts - it is common to put several applications with potentially different authentication mecanism behind the same host,
- The filter just delegates the call except it can replace the request by a wrapped instance forwarding the cookie to the right header,
- getCookies() can return null so ensure we don't fail if it is the case getting a Stream<Cookie>,
- We must extract the security cookie based on its name,
- Finally, when we have a security cookie and the Authorization header is requested, then we return the cookie prefixed by "Bearer " to ensure it passes default setup of Microprofile JWT Auth layer.
This simple filter will make the JWT Auth implementation working well for a server rendered application.
Note on the ordering of the filters
For the previous filter to work, it must be executed before the JWT Auth filter. There are multiple options for that:
- Configure the filters in web.xml which provides a global way to order filters,
- Manage the order programmatically. In other words, in this option to disable the automatic setup of the JWT Auth filter - or you don't add LoginConfig on your Application - and you call the filter yourself in the previous filter. Here what it can look like for Geronimo implementation:
@WebFilter(urlPatterns = "/*", asyncSupported = true) // 1
public class CookieToHeaderFilter implements Filter {
@Inject
@ConfigProperty(name = "app.security.cookieName", defaultValue = "myCookie")
private String cookieName;
private FilterConfig config;
private volatile Filter jwtFilter;
@Override // 2
public void init(final FilterConfig config) {
this.config = config;
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
// 3
if (jwtFilter == null) {
synchronized (this) {
if (jwtFilter == null) {
GeronimoJwtAuthFilter filter = new GeronimoJwtAuthFilter();
filter.init(config);
jwtFilter = filter;
}
}
}
// 4
jwtFilter.doFilter(getRequest(HttpServletRequest.class.cast(request)), response, chain);
}
// same getRequest() as before
}
- In this case we can use @WebFilter to register the filter since the ordering of the two filter is no more ambiguous (we have actually a single filter),
- We store the FilterConfig to be able to lazily initialize the JWT filter - to read lazily the configuration, this is optional but recommended,
- We lazily initialize the JWT filter with the stored filter config,
- Finally we fully delegate to the JWT filter with our wrapped request.
This is exactly the same logic than before but we handle the ordering of the filter explicitly. The drawback is indeed that we are bound to one JWT Auth implementation but the advantage is that the setup is generally simpler. At the end it depends what you want to optimize: simplicity or genericity.
Conclusion
There is not yet a unique portable way to solve the cookie setup with JWT Auth, however you can implement both the Servlet and JAX-RS approach - doing one does not prevent doing the other - which should ensure your code is portable in all implementation of Microprofile (Servlet or JAX-RS based) + JWT Auth stacks.
The specification mentions it will probably support Cookie at some point with a default name of "Bearer". This can also simplify the integration a lot and even enable MVC - the specification - support.
Finally note that Apache Geronimo JWT Auth implementation got enhanced to support cookies out of the box, it will come with the 1.0.3 release (issue is tracked at https://issues.apache.org/jira/browse/GERONIMO-6724).
From the same author:
In the same category: