Security is an integral part of any enterprise application. Security involves two phases i.e. authentication and authorization. Authentication verifies who you are. Authorization verifies what you are authorized to do. In this post, we will learn to build role based basic authentication/ authorization security for REST APIs.
Sections in this post: Background information Important classes/annotations used Building the security interceptor Testing authorization and authentication on REST APIs
Background information
In this post, I will attempt to solve following security problems:
- Get username and password from http request
- Fetch the applicable method security details
- Verify if user is authorized to access the API
- Return valid error codes in case of invalid access
Also, please note that I am leaving following parts on you to implement yourself:
- Access database to fetch password, to verify against password provided in request
- Fetch allowed roles from database for a valid user
I am reusing the code written for ETag usage demo. This project has 2 primary classes:
User.java : The model class which represents the user resource in system
@XmlAccessorType(XmlAccessType.NONE) @XmlRootElement(name = "user") public class User implements Serializable { @XmlAttribute(name = "id") private int id; @XmlAttribute(name="uri") private String uri; @XmlElement(name = "firstName") private String firstName; @XmlElement(name = "lastName") private String lastName; @XmlElement(name="last-modified") private Date lastModified; //Getters and setters }
UserService.java : This class has GET and PUT APIs to fetch/ modify the user resource.
@Path("/user-service") public class UserService { @GET @Path("/users/{id}") public Response getUserById(@PathParam("id") int id, @Context Request req) { Response.ResponseBuilder rb = Response.ok(UserDatabase.getUserById(id)); return rb.build(); } @PUT @Path("/users/{id}") public Response updateUserById(@PathParam("id") int id) { //Update the User resource UserDatabase.updateUser(id); return Response.status(200).build(); } }
I will be using above 2 APIs and secure them in this tutorial.
Important classes/annotations used
JAX-RS provides necessary annotations to implement security in RESTEasy based application. Important annotations are:
- javax.annotation.security.PermitAll: This annotation, when applied on an API, is used to declare that API should be accessed freely by any user. No access restrictions apply on this API.
- javax.annotation.security.DenyAll: This annotation is used to declare that nobody is allowed to access this API irrespective of the roles assigned to any user.
- javax.annotation.security.RolesAllowed: This annotation helps to identify that which roles are necessary for any user, to access this API. If user is validated by user/name and password but does not have role(s) specified by this annotation, he will not be allowed access.
- javax.ws.rs.core.HttpHeaders: An interface that provides access to HTTP header information attached with a http request.
- org.jboss.resteasy.util.Base64: This class helps in encoding and decoding to and from Base64 notations.
Apart from above classes, org.jboss.resteasy.spi.interception.PreProcessInterceptor will be used to create the security interceptor. javax.ws.rs.ext.Provider annotation will be used to register the interceptor with resteasy context.
Building the security interceptor
Before building the interceptor, lets secure the APIs. I have added @PermitAll and @RolesAllowed annotation in GET/ PUT APIs. GET API is open to all i.e. no access restriction. PUT API required a valid user with “ADMIN” capability.
@Path("/user-service") public class UserService { @PermitAll @GET @Path("/users/{id}") public Response getUserById(@PathParam("id") int id, @Context Request req) { Response.ResponseBuilder rb = Response.ok(UserDatabase.getUserById(id)); return rb.build(); } @RolesAllowed("ADMIN") @PUT @Path("/users/{id}") public Response updateUserById(@PathParam("id") int id) { //Update the User resource UserDatabase.updateUser(id); return Response.status(200).build(); } }
The security interceptor is build by implementing org.jboss.resteasy.spi.interception.PreProcessInterceptor interface. This interface has one method which implementing class class need to implement.
public ServerResponse preProcess(HttpRequest request, ResourceMethod methodInvoked) throws Failure, WebApplicationException;
methodInvoked parameter provides access to method which will be invoked as a result of calling the REST API.
request parameter provides access to request headers and parameters passed by client.
The interceptor class is defined as below:
@Provider @ServerInterceptor public class SecurityInterceptor implements PreProcessInterceptor { //more code }
@Provider make the resteasy context scanning to add this class as context class.
@ServerInterceptor annotation adds this class in available interceptors list.
Now, the first step is to check whether API is open for all or closed for all. This can be done by checking @PermitAll and @DenyAll annotations.
Method method = methodInvoked.getMethod(); //Access allowed for all if(method.isAnnotationPresent(PermitAll.class)) { return null; //Return null to continue request processing } //Access denied for all if(method.isAnnotationPresent(DenyAll.class)) { return ACCESS_FORBIDDEN; //Return access denied to user }
Next step is fetch the authorization header from request. From authorization header, we will retrieve the username and password sent by user.
//Get request headers final HttpHeaders headers = request.getHttpHeaders(); //Fetch authorization header final List<String> authorization = headers.getRequestHeader(AUTHORIZATION_PROPERTY); //If no authorization information present; block access if(authorization == null || authorization.isEmpty()) { return ACCESS_DENIED; } //Get encoded username and password final String encodedUserPassword = authorization.get(0).replaceFirst(AUTHENTICATION_SCHEME + " ", ""); //Decode username and password String usernameAndPassword; try { usernameAndPassword = new String(Base64.decode(encodedUserPassword)); } catch (IOException e) { return SERVER_ERROR; } //Split username and password tokens final StringTokenizer tokenizer = new StringTokenizer(usernameAndPassword, ":"); final String username = tokenizer.nextToken(); final String password = tokenizer.nextToken();
Now we will get the roles necessary to access the API from @RolesAllowed annotation.
RolesAllowed rolesAnnotation = method.getAnnotation(RolesAllowed.class); Set<String> rolesSet = new HashSet<String>(Arrays.asList(rolesAnnotation.value()));
rolesSet contains all the roles which can allow the API access.
Now, in final step we have to do two things. First validate the username and password from any database service, and if user is valid then get the role assigned to him. This role will be used to check the authentication success for user.
if(rolesSet.contains(userRole)) { isAllowed = true; }
The complete sourcecode for SecurityInterceptor.java is as given below:
/** * This interceptor verify the access permissions for a user * based on username and passowrd provided in request * */ @Provider @ServerInterceptor public class SecurityInterceptor implements PreProcessInterceptor { private static final String AUTHORIZATION_PROPERTY = "Authorization"; private static final String AUTHENTICATION_SCHEME = "Basic"; private static final ServerResponse ACCESS_DENIED = new ServerResponse("Access denied for this resource", 401, new Headers<Object>());; private static final ServerResponse ACCESS_FORBIDDEN = new ServerResponse("Nobody can access this resource", 403, new Headers<Object>());; private static final ServerResponse SERVER_ERROR = new ServerResponse("INTERNAL SERVER ERROR", 500, new Headers<Object>());; @Override public ServerResponse preProcess(HttpRequest request, ResourceMethod methodInvoked) throws Failure, WebApplicationException { Method method = methodInvoked.getMethod(); //Access allowed for all if(method.isAnnotationPresent(PermitAll.class)) { return null; } //Access denied for all if(method.isAnnotationPresent(DenyAll.class)) { return ACCESS_FORBIDDEN; } //Get request headers final HttpHeaders headers = request.getHttpHeaders(); //Fetch authorization header final List<String> authorization = headers.getRequestHeader(AUTHORIZATION_PROPERTY); //If no authorization information present; block access if(authorization == null || authorization.isEmpty()) { return ACCESS_DENIED; } //Get encoded username and password final String encodedUserPassword = authorization.get(0).replaceFirst(AUTHENTICATION_SCHEME + " ", ""); //Decode username and password String usernameAndPassword; try { usernameAndPassword = new String(Base64.decode(encodedUserPassword)); } catch (IOException e) { return SERVER_ERROR; } //Split username and password tokens final StringTokenizer tokenizer = new StringTokenizer(usernameAndPassword, ":"); final String username = tokenizer.nextToken(); final String password = tokenizer.nextToken(); //Verifying Username and password System.out.println(username); System.out.println(password); //Verify user access if(method.isAnnotationPresent(RolesAllowed.class)) { RolesAllowed rolesAnnotation = method.getAnnotation(RolesAllowed.class); Set<String> rolesSet = new HashSet<String>(Arrays.asList(rolesAnnotation.value())); //Is user valid? if( ! isUserAllowed(username, password, rolesSet)) { return ACCESS_DENIED; } } //Return null to continue request processing return null; } private boolean isUserAllowed(final String username, final String password, final Set<String> rolesSet) { boolean isAllowed = false; //Step 1. Fetch password from database and match with password in argument //If both match then get the defined role for user from database and continue; else return isAllowed [false] //Access the database and do this part yourself //String userRole = userMgr.getUserRole(username); String userRole = "ADMIN"; //Step 2. Verify user role if(rolesSet.contains(userRole)) { isAllowed = true; } return isAllowed; } }
Testing authorization and authentication on REST APIs
To test the security code, deploy the web application in any application server like Tomcat. Now, send following requests:
HTTP GET http://localhost:8080/RESTEasyEtagDemo/user-service/users/1 without username and password
User is able to access the API successfully.
HTTP PUT http://localhost:8080/RESTEasyEtagDemo/user-service/users/1 without username and password
User is not able to access the API.
Add basic authorization credentials
HTTP PUT http://localhost:8080/RESTEasyEtagDemo/user-service/users/1 with username and password added
User is able to access protected API
That’s all in this tutorial. If you have any query or suggestion, drop me a comment.
Happy Learning !!