Authorization and Authentication with AngularJS + Jersey

Workflow

    • User provides credentials which are sent to the server for identification.
    • When the user is identified we perform the following task:
      1. A highly random, un-guessable string is generated.
      2. A JWT is generated with the User ID as subject and the random string as a claim.
      3. Store the JWT in cookie with HttpOnly, Secure flags (our session cookie).
      4. Store the random string in an another cookie with Secure flag only (xsrf cookie).

The Login resource :

@POST
@Path(UserPaths.LOGIN)
@PermitAll
public Response login(@NotNull @Valid LoginRequest request) {
UserProp userProp = userService.login(request);
//Hash UserId for CSRF Protection
String random = Utils.getRandomAlphaNumericString(40);
String token = generateToken(userProp.getId(), userProp.getRole(), random);
NewCookie jwtCookie = addSecuredCookie(COOKIE_ID, token, true);
NewCookie xsrfCookie = addSecuredCookie(CSRF_COOKIE_ID, random, false);
return Response.ok().entity(userProp)
.header(ApiParameters.TOKEN_HEADER, random)
.cookie(jwtCookie, xsrfCookie).build();
}
view raw login hosted with ❤ by GitHub

All ajax requests should append the random string as the X-XSRF-Token Header. The server should reject any request that does not match between the supplied header and the claim of the session cookie.

The Jersey filter :

@Provider
public class AuthenticationFilter implements ContainerRequestFilter {
private static final String AUTHENTICATION_COOKIE = "vedrax-index-ui-sid";
@Context
private ResourceInfo resourceInfo;
@Override
@SuppressWarnings("UnnecessaryReturnStatement")
public void filter(ContainerRequestContext requestContext) throws IOException {
Method method = resourceInfo.getResourceMethod();
if (!method.isAnnotationPresent(PermitAll.class)) {
//Get JWT HttpOnly;Secured cookie
Map<String, Cookie> cookies = requestContext.getCookies();
Cookie authCookie = cookies.get(AUTHENTICATION_COOKIE);
ensureNotNull(authCookie, "Unauthorized: No Authorization cookie was found");
//Create Claims from cookie value
Claims body = getJWTBody(authCookie.getValue());
//Retrieve xsrfToken from body
String xsrfToken = body.get("xsrfToken", String.class);
//Retrieve xsrf from header automatically set by AngularJS
String xsrfHeader = requestContext.getHeaderString("X-XSRF-TOKEN");
ensureNotNull(xsrfHeader, "Unauthorized: No Authorization header was found");
//xsrfToken and xsrfHeader MUST match
ensure(xsrfToken.equals(xsrfHeader), "Unauthorized: security token does not match");
//Retrieve UserId and Scope
final String userId = body.getSubject();
final String scope = body.get("scope", String.class);
//Set securityContext for further use
requestContext.setSecurityContext(new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return new Principal() {
@Override
public String getName() {
return userId;
}
};
}
@Override
public boolean isUserInRole(String role) {
return scope.equals(role);
}
@Override
public boolean isSecure() {
return false;
}
@Override
public String getAuthenticationScheme() {
return null;
}
});
//Check for authorization if annotation
if (method.isAnnotationPresent(RolesAllowed.class)) {
RolesAllowed rolesAnnotation = method.getAnnotation(RolesAllowed.class);
Set<String> rolesSet = new HashSet<>(Arrays.asList(rolesAnnotation.value()));
if (!rolesSet.contains(scope)) {
throwForbiddenException("Access denied for [" + userId + "]");
}
}
}
}
}
view raw auth_filter hosted with ❤ by GitHub

The AngularJS way...

The session cookie is hidden from javascript because of HttpOnly flag. Consequently, we need to make a request (i.e. /api/current) on route change event (AuthService.getUser()). If the session cookie has expired or does not exist the endpoint should return an error (401 - Unauthorized). The $http interceptor will do the rest if any errors.

Angular (v1) | run :

run.$inject = ['$rootScope', '$location', 'AuthService', 'ErrorService'];
function run($rootScope, $location, AuthService, ErrorService) {
$rootScope.$on('$routeChangeStart', function (event, next) {
var roles = next.roles;
if (roles) {
next.resolve = next.resolve || {}; //Add resolve
if (!next.resolve.user) {
next.resolve.user = function () {
return AuthService.getUser()
.then(function (currentUser) {
if (roles.indexOf(currentUser.role) === -1) {
//remove error message if any
ErrorService.removeError();
$location.path('/forbidden');
}
return currentUser;
})
.catch(function () {
$location.path('/login');
});
};
}
}
});
}
AuthInterceptor.$inject = ['$location', '$q'];
function AuthInterceptor($location, $q) {
var service = {
responseError: responseError
};
return service;
function responseError(rejection) {
switch (rejection.status) {
case 401:
$location.path('/login');
break;
case 403:
$location.path('/forbidden');
break;
default:
rejection.handled = true;
$location.path('/error');
}
return $q.reject(rejection);
}
}
view raw angular_run hosted with ❤ by GitHub

Angular (v1) | AuthService.getUser()

function getUser() {
var deferred = $q.defer();
//1. check if authenticated
if (isAuthenticated()) {
deferred.resolve(Session.user);
} else {
var csrfValue = $cookies.get('vedrax-index-ui-csrf');
if (csrfValue) {
//Inject cookie value to request header
$http.defaults.headers.common['X-XSRF-TOKEN'] = csrfValue;
return $http.get(apiUrl + '/current')
.then(function (response) {
Session.create(response.data);
return response.data;
})
.catch(function () {
deferred.reject();
});
} else {
deferred.reject();
}
}
return deferred.promise;
}
view raw angular_getUser hosted with ❤ by GitHub

We check if user is logged in, otherwise we try to retrieve the xsrf cookie in order to make a request to the endpoint by passing the value to the header.




Comments

  1. Hello, thanks a lot for the example, excellent. Source code available somewhere? :)

    ReplyDelete
    Replies
    1. Hi,

      Thanks for your comment. Sorry but for privacy reason I cannot publish the code. In my tuto, you have all the clue to implement your authorization process easily.

      Thanks

      Delete

Post a Comment

Popular posts from this blog

Spring JPA : Using Specification with Projection

Chip input using Reactive Form