First time here? Get an overview of all topics you'll find answers for on this blog here.

Tired of text/plain ? Have a look at my online courses or YouTube channel for more Java, Spring Framework & Jakarta EE content.

Recently I had the requirement for rate-limiting access to specific JAX-RS endpoints and to keep track of the user's current amount of API calls. To solve this problem I asked Adam Bien (@AdamBien) in his monthly Airhacks Q&A about this requirement and he gave me a hint for a possible solution while using the ContainerRequestFilter interface for filtering access to JAX-RS resources.

In this blog post, I'll show you how to implement a simple user-based rate-limiting for a JAX-RS endpoint. I'll deploy the application to Payara and make use of Payara's in-memory H2 database to store the available users and their current/max API budget. To secure the resource I'll use the JSR-375 (Java EE Security API) and define an IdentitiyStore based on a database.

Please note: The provided example contains a database to store information about the rate-limiting. This might not be fast or scalable enough for your use case. Consider using a cache or an in-memory approach instead.

JAX-RS application setup

Let's start with the JAX-RS resource we want to secure. In our example, I am defining one resource which is available under /resources/stocks and returns a hard-coded stock price for Google's Alphabet share:

@Path("stocks") public class StockResource { @GET @RolesAllowed("USER") public Response getAllStocks() { JsonObject json = Json.createObjectBuilder().add("name", "Alphabet Inc.").add("price", 1220.5).build(); return Response.ok(json.toString()).build(); } } 1 2 3 4 5 6 7 8 9 10 @Path ( "stocks" ) public class StockResource { @GET @RolesAllowed ( "USER" ) public Response getAllStocks ( ) { JsonObject json = Json . createObjectBuilder ( ) . add ( "name" , "Alphabet Inc." ) . add ( "price" , 1220.5 ) . build ( ) ; return Response . ok ( json . toString ( ) ) . build ( ) ; } }

@ApplicationPath("/resources") public class JAXRSApplication extends Application { } 1 2 3 @ApplicationPath ( "/resources" ) public class JAXRSApplication extends Application { }

Next, we need @RolesAllowed to only allow users with the role USER to access this endpoint. Unkown users will therefor get a 401 Unauthorized HTTP Status and won't have access to this endpoint.

To define the IdentityStore and the authentication mechanism I make use of some annotations from the new Java EE Security API and define a configuration class:

@DatabaseIdentityStoreDefinition( dataSourceLookup = "jdbc/__default", callerQuery = "SELECT password FROM user WHERE username = ?", groupsQuery = "SELECT role FROM user_roles where username = ?", hashAlgorithm = Pbkdf2PasswordHash.class ) @BasicAuthenticationMechanismDefinition @DeclareRoles({"USER", "ADMIN"}) @ApplicationScoped public class ApplicationSecurityConfig { } 1 2 3 4 5 6 7 8 9 10 11 @DatabaseIdentityStoreDefinition ( dataSourceLookup = "jdbc/__default" , callerQuery = "SELECT password FROM user WHERE username = ?" , groupsQuery = "SELECT role FROM user_roles where username = ?" , hashAlgorithm = Pbkdf2PasswordHash . class ) @BasicAuthenticationMechanismDefinition @DeclareRoles ( { "USER" , "ADMIN" } ) @ApplicationScoped public class ApplicationSecurityConfig { }

The first annotation is responsible for setting up the IdentityStore based on the provided configurations. I am using Payara's default data source and define the required SQL statements for validating an incoming user and for retrieving its roles. In addition, I am selecting the hash algorithm which should be used for hashing the passwords ( Pbkdf2PasswordHash is the default algorithm).

Next, the second annotation is for configuring the authentication mechanism. For simplification, I decided to use a @BasicAuthenticationMechanismDefinition but there is also a form-based or a custom mechanism available.

Furthermore, we use the third annotation to define the available roles for the application's context.

Prepare the database to store rate-limiting information

For storing the user information in the database I modeled two JPA entities: User and UserRoles (the table design is not a best practice as I denormalized the database structure, so don't use this at home):

@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; private long amountOfApiCalls = 0; private long maxApiCallsPerMinute = 10; // getter & setter } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Entity public class User { @Id @GeneratedValue ( strategy = GenerationType . IDENTITY ) private Long id ; private String username ; private String password ; private long amountOfApiCalls = 0 ; private long maxApiCallsPerMinute = 10 ; // getter & setter }

@Entity @Table(name = "user_roles") public class UserRoles { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String role; // getter & setter } 1 2 3 4 5 6 7 8 9 10 11 12 13 @Entity @Table ( name = "user_roles" ) public class UserRoles { @Id @GeneratedValue ( strategy = GenerationType . IDENTITY ) private Long id ; private String username ; private String role ; // getter & setter }

The User table contains two columns for storing the current amount of API calls and the maximum amount of API calls within a minute.

For a quick setup of pre-defined users and roles I am inserting some users on the application's startup:

@Singleton @Startup public class DatabaseSetup { @Inject Pbkdf2PasswordHash pbkdf2PasswordHash; @Resource(lookup = "java:comp/DefaultDataSource") DataSource dataSource; @PostConstruct public void initDefaultUser() { executeUpdate("INSERT INTO user (id, username, password, amountOfApiCalls, maxApiCallsPerMinute) VALUES " + "(1, 'rieckpil', '" + this.pbkdf2PasswordHash.generate("HelloWorld".toCharArray()) + "', 0, 10)"); executeUpdate("INSERT INTO user (id, username, password, amountOfApiCalls, maxApiCallsPerMinute) VALUES " + "(2, 'duke', '" + this.pbkdf2PasswordHash.generate("HelloWorld".toCharArray()) + "', 0, 5)"); executeUpdate("INSERT INTO user (id, username, password, amountOfApiCalls, maxApiCallsPerMinute) VALUES " + "(3, 'john', '" + this.pbkdf2PasswordHash.generate("HelloWorld".toCharArray()) + "', 0, 1)"); executeUpdate("INSERT INTO user_roles (id, username, role) VALUES (1, 'rieckpil', 'USER')"); executeUpdate("INSERT INTO user_roles (id, username, role) VALUES (2, 'rieckpil', 'ADMIN')"); executeUpdate("INSERT INTO user_roles (id, username, role) VALUES (3, 'duke', 'USER')"); executeUpdate("INSERT INTO user_roles (id, username, role) VALUES (4, 'john', 'USER')"); System.out.println("Successfully initialized database with default user"); } private void executeUpdate(String query) { try { this.dataSource.getConnection().createStatement().executeUpdate(query); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException("unable to setup database"); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Singleton @Startup public class DatabaseSetup { @Inject Pbkdf2PasswordHash pbkdf2PasswordHash ; @Resource ( lookup = "java:comp/DefaultDataSource" ) DataSource dataSource ; @PostConstruct public void initDefaultUser ( ) { executeUpdate ( "INSERT INTO user (id, username, password, amountOfApiCalls, maxApiCallsPerMinute) VALUES " + "(1, 'rieckpil', '" + this . pbkdf2PasswordHash . generate ( "HelloWorld" . toCharArray ( ) ) + "', 0, 10)" ) ; executeUpdate ( "INSERT INTO user (id, username, password, amountOfApiCalls, maxApiCallsPerMinute) VALUES " + "(2, 'duke', '" + this . pbkdf2PasswordHash . generate ( "HelloWorld" . toCharArray ( ) ) + "', 0, 5)" ) ; executeUpdate ( "INSERT INTO user (id, username, password, amountOfApiCalls, maxApiCallsPerMinute) VALUES " + "(3, 'john', '" + this . pbkdf2PasswordHash . generate ( "HelloWorld" . toCharArray ( ) ) + "', 0, 1)" ) ; executeUpdate ( "INSERT INTO user_roles (id, username, role) VALUES (1, 'rieckpil', 'USER')" ) ; executeUpdate ( "INSERT INTO user_roles (id, username, role) VALUES (2, 'rieckpil', 'ADMIN')" ) ; executeUpdate ( "INSERT INTO user_roles (id, username, role) VALUES (3, 'duke', 'USER')" ) ; executeUpdate ( "INSERT INTO user_roles (id, username, role) VALUES (4, 'john', 'USER')" ) ; System . out . println ( "Successfully initialized database with default user" ) ; } private void executeUpdate ( String query ) { try { this . dataSource . getConnection ( ) . createStatement ( ) . executeUpdate ( query ) ; } catch ( SQLException e ) { e . printStackTrace ( ) ; throw new RuntimeException ( "unable to setup database" ) ; } } }

The Pbkdf2PasswordHash is available through CDI and can be used anywhere in your code to create a new hash or verify an incoming hash.

For re-setting the API budget for every user, I am using an EJB timer to schedule this task every minute:

@Singleton @Startup public class ApiBudgetRefresher { @PersistenceContext EntityManager entityManager; @Schedule(minute = "*", hour = "*", persistent = false) public void updateApiBudget() { System.out.println("-- refreshing API budget for all users"); List<User> userList = entityManager.createQuery("SELECT u FROM User u", User.class).getResultList(); for (User user : userList) { user.setAmountOfApiCalls(0); } System.out.println("-- successfully refreshed API budget for all users"); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Singleton @Startup public class ApiBudgetRefresher { @PersistenceContext EntityManager entityManager ; @Schedule ( minute = "*" , hour = "*" , persistent = false ) public void updateApiBudget ( ) { System . out . println ( "-- refreshing API budget for all users" ) ; List <User> userList = entityManager . createQuery ( "SELECT u FROM User u" , User . class ) . getResultList ( ) ; for ( User user : userList ) { user . setAmountOfApiCalls ( 0 ) ; } System . out . println ( "-- successfully refreshed API budget for all users" ) ; } }

Rate-limiting the access with a JAX-RS filter

Now comes the interesting part for the implementation of the ContainerRequestFilter interface:

@Provider public class RateLimitingFilter implements ContainerRequestFilter { @PersistenceContext EntityManager entityManager; @Transactional @Override public void filter(ContainerRequestContext requestContext) throws IOException { SecurityContext securityContext = requestContext.getSecurityContext(); String username = securityContext.getUserPrincipal().getName(); User user = entityManager.createQuery("SELECT u FROM User u WHERE u.username=:username", User.class).setParameter( "username", username).getSingleResult(); if (user.getAmountOfApiCalls() >= user.getMaxApiCallsPerMinute()) { requestContext.abortWith(Response.status(Response.Status.TOO_MANY_REQUESTS).build()); } user.setAmountOfApiCalls(user.getAmountOfApiCalls() + 1); System.out.println(user); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Provider public class RateLimitingFilter implements ContainerRequestFilter { @PersistenceContext EntityManager entityManager ; @Transactional @Override public void filter ( ContainerRequestContext requestContext ) throws IOException { SecurityContext securityContext = requestContext . getSecurityContext ( ) ; String username = securityContext . getUserPrincipal ( ) . getName ( ) ; User user = entityManager . createQuery ( "SELECT u FROM User u WHERE u.username=:username" , User . class ) . setParameter ( "username" , username ) . getSingleResult ( ) ; if ( user . getAmountOfApiCalls ( ) > = user . getMaxApiCallsPerMinute ( ) ) { requestContext . abortWith ( Response . status ( Response . Status . TOO_MANY_REQUESTS ) . build ( ) ) ; } user . setAmountOfApiCalls ( user . getAmountOfApiCalls ( ) + 1 ) ; System . out . println ( user ) ; } }

The class provides an implementation for the filter() method and is recognized from JAX-RS through the @Provider annotation. To update all current users I am injecting the EntityManager to this filter. As this is not an EJB we need the @Transactional annotation here to explicitly wrap this code in a transaction. The code will first retrieve the username from the Principal. In addition, we'll then check if the current budget of the user allows a new API call. If there is not enough budget the filter returns with an HTTP Status code 429 – Too Many Requests.

Accessing the endpoint with the credentials for rieckpil as a base64 encoded string less than eleven times will result in a valid result:

$ curl -i -H 'Authorization: Basic cmllY2twaWw6SGVsbG9Xb3JsZA==' http://localhost:8080/api-rate-limiting/resources/stocks HTTP/1.1 200 OK Server: Payara Server 5.182 #badassfish X-Powered-By: Servlet/4.0 JSP/2.3 (Payara Server 5.182 #badassfish Java/Oracle Corporation/1.8) Content-Type: text/plain Content-Length: 39 X-Frame-Options: SAMEORIGIN {"name":"Alphabet Inc.","price":1220.5} 1 2 3 4 5 6 7 8 9 $ curl -i -H 'Authorization: Basic cmllY2twaWw6SGVsbG9Xb3JsZA==' http://localhost:8080/api-rate-limiting/resources/stocks HTTP/1.1 200 OK Server: Payara Server 5.182 #badassfish X-Powered-By: Servlet/4.0 JSP/2.3 (Payara Server 5.182 #badassfish Java/Oracle Corporation/1.8) Content-Type: text/plain Content-Length: 39 X-Frame-Options: SAMEORIGIN {"name":"Alphabet Inc.","price":1220.5}

After the tenth API call, the user will get the following result and has to wait one minute:

$ curl -i -H 'Authorization: Basic cmllY2twaWw6SGVsbG9Xb3JsZA==' http://localhost:8080/api-rate-limiting/resources/stocks HTTP/1.1 429 Too Many Requests Server: Payara Server 5.182 #badassfish X-Powered-By: Servlet/4.0 JSP/2.3 (Payara Server 5.182 #badassfish Java/Oracle Corporation/1.8) Content-Language: Content-Type: text/html Content-Length: 1100 X-Frame-Options: SAMEORIGIN // error HTML page 1 2 3 4 5 6 7 8 9 10 $ curl -i -H 'Authorization: Basic cmllY2twaWw6SGVsbG9Xb3JsZA==' http://localhost:8080/api-rate-limiting/resources/stocks HTTP/1.1 429 Too Many Requests Server: Payara Server 5.182 #badassfish X-Powered-By: Servlet/4.0 JSP/2.3 (Payara Server 5.182 #badassfish Java/Oracle Corporation/1.8) Content-Language: Content-Type: text/html Content-Length: 1100 X-Frame-Options: SAMEORIGIN // error HTML page

You can try this example on your local machine as I provided a Docker image in the GitHub repository.

If you are new to JAX-RS, start with this introduction. For further JAX-RS examples, have a look at the following overview page.

Happy rate-limiting with JAX-RS,

Phil