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:
- A highly random, un-guessable string is generated.
- A JWT is generated with the User ID as subject and the random string as a claim.
- Store the JWT in cookie with HttpOnly, Secure flags (our session cookie).
- Store the random string in an another cookie with Secure flag only (xsrf cookie).
The Login resource :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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(); | |
} |
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 :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 + "]"); | |
} | |
} | |
} | |
} | |
} |
The AngularJS way...
Angular (v1) | run :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
Angular (v1) | AuthService.getUser()
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
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.
Hello, thanks a lot for the example, excellent. Source code available somewhere? :)
ReplyDeleteHi,
DeleteThanks 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