Header based stateless token authentication for JAX-RS
Luckily JAX-RS is simply layered on top of Servlets, and one can therefore just use JASPIC's authentication modules for the Servlet Container Profile. There's thus not really a need for a separate REST profile, as there is for SOAP web services.
While using the same basic technologies as authentication modules for web applications, the requirements for modules that are to be used for JAX-RS are a bit different.
JAX-RS is often used to implement an API that is used by scripts. Such scripts typically do not engage into an authentication dialog with the server, i.e. it's rare for an API to redirect to a form asking for credentials, let alone asking to log-in with a social provider.
An even more fundamental difference is that in web apps it's commonplace to establish a session for among others authentication purposes. While possible to do this for JAX-RS as well, it's not exactly a best practice. Restful APIs are supposed to be fully stateless.
To prevent the need for going into an arbitrary authentication dialog with the server, it's typically for scripts to send their credentials upfront with a request. For this BASIC authentication can be used, which does actually initiates a dialog albeit a standardised one. An other option is to provide a token as either a request parameter or as an HTTP header. It should go without saying that in both these case all communication should be done exclusively via https.
Preventing a session to be created can be done in several ways as well. One way is to store the authentication data in an encrypted cookie instead of storing that data in the HTTP session. While this surely works it does feel somewhat weird to "blindly" except the authenticated identity from what the client provides. If the encryption is strong enough it *should* be okayish, but still. Another method is to quite simply authenticate every time over again with each request. This however has its own problem, namely the potential for bad performance. An in-memory user store will likely be very fast to authenticate against, but anything involving an external system like a database or ldap server probably is not.
The performance problem of authenticating with each request can be mitigated though by using an authentication cache. The question is then whether this isn't really the same as creating a session?
While both an (http) session and a cache consume memory at the server, a major difference between the two is that a session is a store for all kinds of data, which includes state, but a cache is only about data locality. A cache is thus by definition never the primary source of data.
What this means is that we can throw data away from a cache at arbitrary times, and the client won't know the difference except for the fact its next request may be somewhat slower. We can't really do that with session data. Setting a hard limit on the size of a cache is thus a lot easier for a cache then it is for a session, and it's not mandatory to replicate a cache across a cluster.
Still, as with many things it's a trade off; having zero data stored at the server, but having a cookie send along with the request and needing to decrypt that every time (which for strong encryption can be computational expensive), or having some data at the server (in a very manageable way), but without the uneasiness of directly accepting an authenticated state from the client.
Here we'll be giving an example for a general stateless auth module that uses header based token authentication and authenticates with each request. This is combined with an application level component that processes the token and maintains a cache. The auth module is implemented using JASPIC, the Java EE standard SPI for authentication. The example uses a utility library that I'm incubating called OmniSecurity. This library is not a security framework itself, but provides several convenience utilities for the existing Java EE security APIs. (like OmniFaces does for JSF and Guava does for Java)
One caveat is that the example assumes CDI is available in an authentication module. In practice this is the case when running on JBoss, but not when running on most other servers. Another caveat is that OmniSecurity is not yet stable or complete. We're working towards an 1.0 version, but the current version 0.6-ALPHA is as the name implies just an alpha version.
The module itself look as follows:
public class TokenAuthModule extends HttpServerAuthModule { private final static Pattern tokenPattern = compile("OmniLogin\\s+auth\\s*=\\s*(.*)"); @Override public AuthStatus validateHttpRequest(HttpServletRequest request, HttpServletResponse response, HttpMsgContext httpMsgContext) throws AuthException { String token = getToken(request); if (!isEmpty(token)) { // If a token is present, authenticate with it whether this is strictly required or not. TokenIdentityStore tokenIdentityStore = getReferenceOrNull(TokenIdentityStore.class); if (tokenIdentityStore != null) { if (tokenIdentityStore.authenticate(token)) { return httpMsgContext.notifyContainerAboutLogin(tokenIdentityStore.getUserName(), tokenIdentityStore.getApplicationRoles()); } } } if (httpMsgContext.isProtected()) { return httpMsgContext.responseNotFound(); } return httpMsgContext.doNothing(); } private String getToken(HttpServletRequest request) { String authorizationHeader = request.getHeader("Authorization"); if (!isEmpty(authorizationHeader)) { Matcher tokenMatcher = tokenPattern.matcher(authorizationHeader); if (tokenMatcher.matches()) { return tokenMatcher.group(1); } } return null; } }
In the example above the authentication module extracts a token from the request. If one is present, it obtains a reference to a TokenIdentityStore, which does the actual authentication of the token and provides a username and roles if the token is valid. It's not strictly necessary to have this separation and the authentication module could just as well contain all required code directly. However, just like the separation of responsibilities in MVC, it's typical in authentication to have a separation between the mechanism and the repository. The first contains the code that does interaction with the environment (aka the authentication dialog, aka authentication messaging), while the latter doesn't know anything about an environment and only keeps a collection of users and roles that are accessed via some set of credentials (e.g. username/password, keys, tokens, etc).
If the token is found to be valid, the authentication module retrieves the username and roles from the identity store and passes these to the container. Whenever an authentication module does this, it's supposed to return the status "SUCCESS". By using the HttpMsgContext this requirement is largely made invisible; the code just returns whatever HttpMsgContext.notifyContainerAboutLogin returns.
If authentication did not happen for whatever reason, it depends on whether the resource (URL) that was accessed is protected (requires an authenticated user) or is public (does not require an authenticated user). In the first situation we always return a 404 to the client. This is a general security precaution. According to HTTP we should actually return a 403 here, but if we did users can attempt to guess what the protected resources are. For applications where it's already clear what all the protected resources are it would make more sense to indeed return that 403 here. If the resource is a public one, the code "does nothing". Since authentication modules in Java EE need to return something and there's no status code that indicates nothing should happen, in fact doing nothing requires a tiny bit of work. Luckily this work is largely abstracted by HttpMsgContext.doNothing().
Note that the TokenAuthModule as shown above is already implemented in the OmniSecurity library and can be used as is. The TokenIdentityStore however has to be implemented by user code. An example of an implementation is shown below:
@RequestScoped public class APITokenAuthModule implements TokenIdentityStore { @Inject private UserService userService; @Inject private CacheManager cacheManager; private User user; @Override public boolean authenticate(String token) { try { Cache<String, User> usersCache = cacheManager.getDefaultCache(); User cachedUser = usersCache.get(token); if (cachedUser != null) { user = cachedUser; } else { user = userService.getUserByLoginToken(token); usersCache.put(token, user); } } catch (InvalidCredentialsException e) { return false; } return true; } @Override public String getUserName() { return user == null ? null : user.getUserName(); } @Override public List<String> getApplicationRoles() { return user == null ? emptyList() : user.getRoles(); } // (Two empty methods omitted) }
Installing the authentication module can be done during startup of the container via a Servlet context listener as follows:
@WebListener public class SamRegistrationListener extends BaseServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { Jaspic.registerServerAuthModule(new TokenAuthModule(), sce.getServletContext()); } }
curl -vs -H "Authorization: OmniLogin auth=ABCDEFGH123" https://localhost:8080/api/foo
As shown in this article, adding an authentication module for JAX-RS that's fully stateless and doesn't store an authenticated state on the client is relatively straightforward using Java EE authentication modules. Big caveats are that the most straightforward approach uses CDI which is not always available in authentication modules (in WildFly it's available), and that the example uses the OmniSecurity library to simplify some of JASPIC's arcane native APIs, but OmniSecurity is still only in an alpha status.
Arjan Tijms
Arjan, I see in your API you use messageInfo.getMap().get("javax.security.auth.message.MessagePolicy.isMandatory") to know if a resource is protected or not, but does this works on Glassfish, since you said in another post that javax.security.auth.message.MessagePolicy.isMandatory is not set to false in MessageInfo Map for non-protected resources for certain Servers, including Glassfish?
ReplyDelete>but does this works on Glassfish, since you said in another post that javax.security.auth.message.MessagePolicy.isMandatory is not set to false in MessageInfo Map for non-protected resources for certain Servers, including Glassfish?
DeleteYes, it does, since if the field is available AND "true", the outcome will be true. If the field is NOT available a null will be passed into Boolean.valueof, after the which the outcome will be false. Finally, if the field is available AND "false", the outcome will be false too.
Hope this makes it more clear ;)
Clear as water, thank you!
Delete