Securing a REST Service
Originally published on 05 Sep 2018
Last updated on 07 Sep 2018
If you're building a REST service, then that REST service will expose some kind of data or will allow some kind of interactions with a server. For instance, consider a Facebook REST service that allows you to retrieve your chat history. Naturally you don't want just anyone looking at that history, hence the need for security.
What are the Concepts Involved with REST Service Security?
For Java EE security there are a few basic concepts involved:
- Caller - A user or script that calls the REST Service.
- Role - An opaque attribute or ID associated with a specific caller. This is used to refer to a group of callers in an abstract way. For example, "all callers with the role 'xyz'."
- Authenticating - Callers proving their identity, largely synonymous to "logging in". For example, "Is the caller claiming to be Sarah indeed Sarah?"
- Authorising - Determining whether a caller has access to something. For example, "Is Sarah allowed to see Mike's photos?"
- Authentication Mechanism - The specific way in which the server interacts with the caller in order to authenticate that caller. For example, via the BASIC authentication mechanism a caller sends a username/password combination via an HTTP header, but with the JWT authentication mechanism a JWT token is send.
- Identity Store - A logical unit that is capable of validating credentials and returning a caller name and zero or more roles associated with that caller. Often, but not necessarily, the identity store provides access to (external) storage containing the details of callers, their credentials and their roles. For example, a database-, file- or LDAP identity store. All this data can also be stored within the effective credential, which is the case for a JWT token.
How to Secure a REST Service
For Java EE Security / the Payara Platform, there are a couple of options:
- REST Services can be secured by defining constraints on the URL-, endpoint (resource class)-, or method level.
- An authentication mechanism can be chosen from those defined by the Servlet API, by the Java EE Security API, or a custom (application provided) one can be used.
- An identity store can be left undefined by the application and left to be configured within Payara Server (for example, using the admin console or CLI), one can be chosen from those defined by the Java EE Security API, or a custom (application provided) one can be used.
Which Authentication Mechanisms are Available in the Payara Platform?
The Payara Platform ships with the authentication mechanisms defined by the Servlet-, Java EE Security-, MicroProfile- and Payara APIs. These all can be used largely interchangeably. Note that not all of them are primarily useful for REST authentication, but are instead more suited for interactive websites (specifically the FORM based ones).
Servlet
- BASIC
- FORM
- DIGEST
- CERTIFICATE
Java EE Security
- BASIC
- FORM
- Custom FORM
MicroProfile
- JWT
Payara API
- OAuth2
- OpenID Connect (coming in 5.183)
- Yubikey
Which Identity Stores are Available in the Payara Platform?
The Payara Platform ships with the identity stores defined by the Java EE Security API, and its own internal ones (a JAAS LoginModule/GlassFish Realm combination) that can be configured via the admin console, CLI or domain.xml. In Payara 5.182 and before identity stores from Java EE Security can be paired with authentication mechanisms from Java EE Security, MicroProfile and the Payara API, but not with those from Servlet. Likewise, the internal ones can only be paired with the authentication mechanisms from Servlet. Payara 5.182will likely make these all interchangeable. Note that MicroProfile JWT has its own identity store, but it can be paired if needed with additional identity stores if extra roles beyond those provided by the JWT token are needed.
Java EE Security
- LDAP
- DataBase
Payara Internal
- File
- LDAP
- JDBC (DataBase)
- Digest
- Solaris
Example
For our first example we'll be demonstrating Java EE Security BASIC authentication with a custom (application provided) identity store.
We'll start with defining the REST Service (JAX-RS resource) itself as follows:
@Path("/resource") @Produces(TEXT_PLAIN) public class Resource { @GET @Path("hi") @RolesAllowed("a") public String hi() { return "hi!"; } }
In this simple example we just have a single resource method returning the text "hi!". Special about this REST Service is the use of the @RolesAllowed annotation, which causes the resource method to be secured, and thus only accessible for callers who are associated with (are in the/have the) role "a". Note that this annotation is supported out of the box by Payara, but may not be supported directly on other servers, or may have to be activated explicitly on those other servers.
Next we'll create an activator class that activates JAX-RS itself, but most importantly for this example defines that we want to use the BASIC authentication mechanism from Java EE:
@ApplicationScoped @ApplicationPath("/rest") @DeclareRoles({ "a", "b" }) @BasicAuthenticationMechanismDefinition(realmName = "foo-ee") public class JaxRsActivator extends Application { }
The @BasicAuthenticationMechanismDefinition annotation causes Payara to put a CDI bean implementing the BASIC authentication mechanism in service.
The last piece is the custom identity store, which contains the data for a single test caller:
@ApplicationScoped public class TestIdentityStore implements IdentityStore { public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) { if (usernamePasswordCredential.compareTo("test", "secret")) { return new CredentialValidationResult("test", new HashSet<>(asList("a", "b"))); } return INVALID_RESULT; } }
In this minimum identity store we recognise a caller with as credentials a username "test" and a password "secret". For this caller we'll return the same username and the groups "a" and "b" (which are mapped to roles of the same name by default, so we don't have to worry about groups/roles now).
When we bundle this together into a war called "test" and deploy it to a default Payara Server instance, we can request http://localhost:8080/test/rest/resource/hi and the browser should respond with a dialog asking for the credentials. If we enter "test" and "secret" we'll see "hi!" on our screen, otherwise we'll see either a 401 or the browser will redisplay the dialog.
Demonstrating that much of our code stays the same when switching authentication mechanisms between EE Security and Servlet, we'll also show the conceptual same BASIC mechanism configured via Servlet. For that the REST Service stays the same, but we remove the authentication mechanism annotation from the activator class:
@ApplicationScoped @ApplicationPath("/rest") @DeclareRoles({ "a", "b" }) public class JaxRsActivator extends Application { }
In its place we add a WEB-IN/web.xml file with the following contents:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <login-config> <auth-method>BASIC</auth-method> <realm-name>foo-servlet</realm-name> </login-config> </web-app>
Note that the realm name, contrary to a very persistent public belief, is not officially specified to indicate which identity store is being used. The string here ("foo-servlet") is supposed to be only used in the header that is sent to the caller (and which for example a browser renders as a dialog box). But in Payara it can optionally indicate which Payara Realm/LoginModule combo is used. If it's an unknown Payara Realm it will still be sent to the browser, but the default Payara realm will be used.
The next thing to do is configuring Payara's global identity store, which in Payara terms is called a realm. The default global realm is File, and since that's exactly the one that's easiest for this example we don't have to actually select it. We do have to a user called "test" to it. With Payara running, execute the following command from [payara install dir]/bin:
./asadmin create-file-user --groups a test
When asked for the password enter "secret".
Now when deploying this again and requesting http://localhost:8080/test/rest/resource/hi once more, we should see the same result as with the first variant of this application.
The full source of both variants can be found on GitHub, along with an automated tests that demonstrates how the applications are supposed to be called:
Related Posts
A Preview of Jakarta REST (JAX-RS) 4.0 in Jakarta EE 11
Published on 13 Nov 2024
by Luqman Saeed
0 Comments
The latest version of Jakarta REST (formerly Java API for RESTful Web Services, aka JAX-RS), Jakarta REST 4.0, will bring some notable improvements and changes as part of Jakarta EE 11. This release focuses on modernizing the specification by ...
Master Your HATEOAS as a Jakarta EE Developer
Published on 23 Oct 2024
by Chiara Civardi
0 Comments