Pairing AngularJS and Java EE 7 for authentication
In this post I will illustrate how to pair your AngularJS SPA (single-page application) with a Java EE backend providing a JAX-RS 2.0 API. Especially the token based authentication part is described in detail. My intention is a more general approach showing the method I use. I will not discuss the whole code, there are many tutorials out there doing a great job covering AngularJS basics.
Infrastructure
In this setup a nginx webserver runs on port 80, providing the AngularJS SPA and static content. Further, nginx acts as a proxy to connect to a Java EE 7 app deployed to a WildFly running on port 8080.
This way, the setup is optimzed to serve the static content by a high performace webserver and a dedicated application server to work in the background. Furthermore, it’s easy to change the backend technology without even thouching the frontend.
Natively the backend is available at http://localhost:8080/rebackend/resource/. My goal is to make the provided API available at http://localhost/api/. This way CORS issues are prevented, because every resource will be available under the same port and host at http://localhost/*.
The following part shows the nginx configuration in the file nginx.conf. Starting from line 6, you see the proxy setup for /api/. The parameter proxy_pass points to the context root of the REST API. Line 14 is the webroot and default name for the index file is provided.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
http { server { listen 80; server_name www.example.com; location /api/ { proxy_pass [PATH_TO_YOUR_REST_APP]; # rest-api proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { root [PATH_TO_ANGULAR_APP_ROOT]; index index.html; } } } |
Application architecture and authentication
If the AngularJS application interacts with a RESTful service and, in particular, authenticates against it, some authentication information has to be added to each request.
Frontend
Ok, let’s start with the login process. The user enters username and password in the frontend and submits the login form. In AngularJS, a Login Controller handles this action.
I use the array notation to define the dependencies AngularJS will automatically inject, otherwise the minification process will cause strange behaviour. In any case, there are grunt plugins taking care of this.
All the authentication logic is implemented in the authFactory . During the login process, the credentials are send to the backend. In case of a successful login, AngularJS will store the authentication data in the factory too.
1 2 3 4 5 6 7 8 9 10 |
.controller('LoginCtrl', ['$scope', 'authFactory', function LoginCtrl($scope, authFactory) { $scope.login = function (user) { authFactory.login(user).success(function (data) { authFactory.setAuthData(data); // Redirect etc. }).error(function () { // Error handling }); }; }]) |
The authFactory posts the user credentials to the REST backend via a post request.
1 2 3 4 5 6 7 8 9 10 11 12 |
.factory('authFactory', ['$rootScope', '$http', function ($rootScope, $http) { var authFactory = { authData: undefined }; authFactory.login = function (user) { return $http.post('http://localhost/api/auth/', user); }; return authFactory; }]) |
Backend
To make life easier, the request data is automatically wrapped by an AuthLoginElement containing the login credentials wich the frontend sends to the API.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@XmlRootElement public class AuthLoginElement implements Serializable { private String username; private String password; public AuthLoginElement(String username, String password) { this.username = username; this.password = password; } // Getters and setters } |
The login request is handled by the REST resource AuthResource available at /auth/. The resource produces and consumes JSON, as this is the easiest to work with in the frontend. Access to /auth/login is permitted to all by using the annotation @PermitAll.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@Path("/auth") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class AuthResource { @EJB AuthService authService; @POST @Path("login") @PermitAll public AuthAccessElement login(@Context HttpServletRequest request, AuthLoginElement loginElement) { AuthAccessElement accessElement = authService.login(loginElement); if (accessElement != null) { request.getSession().setAttribute(AuthAccessElement.PARAM_AUTH_ID, accessElement.getAuthId()); request.getSession().setAttribute(AuthAccessElement.PARAM_AUTH_TOKEN, accessElement.getAuthToken()); } return accessElement; } } |
The credentials are processed by the AuthService. In this case username and password are checked against the database in this example. If the check is successful, a token is generated and stored with the user profile (see lines 6, 8-9).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Stateless(name = "AuthService") public class AuthServiceBean implements AuthService { @Override public AuthAccessElement login(AuthLoginElement loginElement) { User user = userService.findByUsernameAndPassword(loginElement.getUsername(), loginElement.getPassword()); if (user != null) { user.setAuthToken(UUID.randomUUID.toString()); userService.save(user); return new AuthAccessElement(loginElement.getUsername(), authToken, user.getAuthRole()); } return null; } } |
The response, generated by AuthResource.login(), will include the username, generated token and role. Again, the data is wrapped by an object, in this case the AuthAccessElement to be returned.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@XmlRootElement public class AuthAccessElement implements Serializable { public static final String PARAM_AUTH_ID = "auth-id"; public static final String PARAM_AUTH_TOKEN = "auth-token"; private String authId; private String authToken; private String authPermission; public AuthAccessElement() { } public AuthAccessElement(String authId, String authToken, String authPermission) { this.authId = authId; this.authToken = authToken; this.authPermission = authPermission; } // Getters and setters } |
The role returned by the backend can be used to check the authorization of the user to see pages or to take actions in the AngularJS App.
Frontend
As mentioned before, in case of a successful login LoginCtrl.login stores the username, token and role to the authFactory by authFactory.setAuthData(data) (see LoginCtrl above in line 4).
The following snippet shows the essential part of the authFactory handling the authentication data after the login. authFactory.setAuthData() saves the parameter to authData.
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 |
.factory('authFactory', ['$rootScope', '$http', function ($rootScope, $http) { var authFactory = { authData: undefined }; authFactory.setAuthData = function (authData) { this.authData = { authId: authData.authId, authToken: authData.authToken, authPermission: authData.authPermission }; $rootScope.$broadcast('authChanged'); }; authFactory.getAuthData = function () { return this.authData; }; authFactory.isAuthenticated = function () { return !angular.isUndefined(this.getAuthData()); }; return authFactory; }]) |
Knowing authFactory.authData is set in case of a successful login, it’s easy to check if the user is logged in or not by authFactory.isAuthenticated().
Ok, so far, so good, we are logged in. But, how do we authenticate at every single REST request we send to a restricted service? To solve this problem, in AngularJS it is possible to add so called HTTP interceptors. These interceptors will be called on every defined http event and allow manipulating the requests and reponses. Using this mechanism, we will add our custom information to the request. The following block is such a interceptor. In this case it will be called on every request the Application sends (line 3).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
.factory('authHttpRequestInterceptor', ['$rootScope', '$injector', 'authFactory', function ($rootScope, $injector, authFactory) { var authHttpRequestInterceptor = { request: function ($request) { var authFactory = $injector.get('authFactory'); if (authFactory.isAuthenticated())) { $request.headers['auth-id'] = authFactory.getAuthData().authId; $request.headers['auth-token'] = authFactory.getAuthData().authToken; } return $request; }; return authHttpRequestInterceptor; }]) |
So, in case we are logged in (remember authFactory.isAuthenticated()) we have to manipulate the request data (lines 6-7) by adding the username and token as auth-id and auth-token to the request.
The following snippet adds the previously defined interceptor authHttpRequestInterceptor to the chain.
1 2 3 |
.config(function ($httpProvider) { $httpProvider.interceptors.push('authHttpRequestInterceptor'); }) |
Ok, so we are done on the frontend part.
Backend
Now, the ingoing request has to be checked for the payload added earlier. This can be done by implementing ContainerRequestFilter to intercept the request. All interceptors and filters registered with the @Provider annotation are globally enabled for all resources. At deployment time, the server scans the deployment units for @Provider annotations and automatically registers all extensions before the activation of the application.
First, a proper response and message is defined in case the authentication check fails (line 5). In AuthSecurityInterceptor.filter() the username and token is gathered from the request (line 22-23). After that, the method invoked is detected and wether the method is annoted with @RolesAllowed. If this is the case, the user needs to be logged in and having the rigth role.
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 |
@Provider public class AuthSecurityInterceptor implements ContainerRequestFilter { // 401 - Access denied private static final Response ACCESS_UNAUTHORIZED = Response.status(Response.Status.UNAUTHORIZED).entity("Not authorized.").build(); @EJB AuthService authService; @Context private HttpServletRequest request; @Context private ResourceInfo resourceInfo; @Override public void filter(ContainerRequestContext requestContext) throws IOException { // Get AuthId and AuthToken from HTTP-Header. String authId = requestContext.getHeaderString(AuthAccessElement.PARAM_AUTH_ID); String authToken = requestContext.getHeaderString(AuthAccessElement.PARAM_AUTH_TOKEN); // Get method invoked. Method methodInvoked = resourceInfo.getResourceMethod(); if (methodInvoked.isAnnotationPresent(RolesAllowed.class)) { RolesAllowed rolesAllowedAnnotation = methodInvoked.getAnnotation(RolesAllowed.class); Set<String> rolesAllowed = new HashSet<>(Arrays.asList(rolesAllowedAnnotation.value())); if (!authService.isAuthorized(authId, authToken, rolesAllowed)) { requestContext.abortWith(ACCESS_UNAUTHORIZED); } } } } |
The authorization check is performed by AuthServiceBean.isAuthorized(). Firstly, it is verified that the user is logged in. Afterwards the roles allowed for the called function are tested against the user roles (lines 9-11). Imagine, for example, an annotation like @RolesAllowed({"ADMIN"}).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Stateless(name = "AuthService") public class AuthServiceBean implements AuthService { @EJB UserService userService; @Override public boolean isAuthorized(String authId, String authToken, Set<String> rolesAllowed) { User user = userService.findByUsernameAndAuthToken(authId, authToken); if (user != null) { return rolesAllowed.contains(user.getAuthRole()); } else { return false; } } } |
In case the authorization check fails the request will be aborted by requestContext.abortWith(ACCESS_UNAUTHORIZED); (line 30 of AuthSecurityInterceptor). Otherwise, the request will be processed as intended.
Conclusion
As you can see, it is pretty straightforward to pair an AngularJS application with a Java EE REST Api. This applies also to the implementation of a token based authentication mechanism.