I'm working on a experimental code which allows users to authorize using JWT's jjwt
library. Here's what I have done so far on authentication and authorization flow.
- Can I get some improvement ideas on this?
- And what security issues can be found in this?
- Any other best practices and suggestions are appreciated.
- Is it a good idea to show specific error responses to client when token or token header is altered or invalid ?
User login
- User enters login credentials. (User login page is being displayed to every user)
- Authentication service validates user's existence and login credentials.
- If user has successfully authenticated, then creates
jwt
,HttpSession
objects for user and acookie
which hasjwt
as its value.
In client side
- In client side (web browser),
javascript
reads thiscookie
and sends to server it's value (jwt
) with every request as request headerAuthorization: Bearer 'jwt'
JWT interceptor
AuthenticationInterceptor
works as the following snippets.
in preHandle()
method
read the authorization header
String header = request.getHeader("Authorization"); String request_url = request.getServletPath();
- if header is not found,
// Authorisation header not found - unauthorised, // redirects to another URL that creates appropriate response if (header == null || header.isEmpty()) { LOGGER.debug("INTERCEPTOR-UNAUTHORIZED authorization header not found"); response.sendRedirect( new StringBuilder() .append(request.getContextPath()) .append("/api/user/authentication") .append("?e=3") .toString() ); return false; }
- if header value malformed (does not contain
Bearer
string)// Authorisation header not invalid - unauthorised // redirects to another URL that creates appropriate response if (!header.startsWith("Bearer ")) { LOGGER.debug("INTERCEPTOR-UNAUTHORIZED authorization header not found or malformed"); response.sendRedirect( new StringBuilder() .append(request.getContextPath()) .append("/api/user/authentication") .append("?e=3") .toString() ); return false; }
- when an invalid token found
String cleanedHeader = header.replace("Bearer ", "").trim(); // Authorisation header value expired/invalid - unauthorised // redirects to another URL that creates appropriate response if (!getJwtProviderService().validateToken(cleanedHeader)) { LOGGER.debug("INTERCEPTOR-UNAUTHORIZED authorization token is not valid or altered"); response.sendRedirect( new StringBuilder() .append(request.getContextPath()) .append("/api/user/authentication") .append("?e=1") .toString() ); return false; }
- Authorization part
/** * method 'authenticateUser(HttpServletRequest, String)' will do the * validation part of jwt and http session of this request. * * if jwt is successfully validated and the http session exists, and * these details are matched, it will return 'VIA_SESSION_AND_TOKEN' * enum. * * if jwt is successfully validated but http session has expired, then * it will return 'VIA_AUTHENTICATED_TOKEN' enum. * * if jwt is invalid, it will return 'NOT_AUTHENTICATED' enum. */ AuthenticationOption authenticateUser = getJwtProviderService().authenticateUser(request, cleanedHeader); // Authorisation header value expired/invalid/user not authenticated - unauthorised // redirects to another URL that creates appropriate response if (AuthenticationOption.NOT_AUTHENTICATED.equals(authenticateUser)) { LOGGER.debug("INTERCEPTOR-UNAUTHORIZED authorization token is not valid or altered"); response.sendRedirect( new StringBuilder() .append(request.getContextPath()) .append("/api/user/authentication") .append("?e=2") .toString() ); return false; } switch (authenticateUser) { case VIA_SESSION_AND_TOKEN: { // LOGGER.debug("INTERCEPTOR-AUTHORIZED via token and session"); String userType = getUserType(request); switch (userType) { case "ADMIN": return request_url.contains("/admin/") || request_url.contains("/api/") || request_url.contains("/user/"); case "USER": return request_url.contains("/user/") || request_url.contains("/api/"); default: return false; } } case VIA_AUTHENTICATED_TOKEN: { LOGGER.debug("INTERCEPTOR-AUTHORIZED via token"); //user has authorised via token, then creates new http session boolean create = createNewSession(request, cleanedHeader); //if new session has successfully created then re-directs //to same URL to execute authorization again. if (create) { response.sendRedirect( new StringBuilder() .append(request.getContextPath()) .append(request.getServletPath()) .toString() ); } } default: return false; } }
read user role from session object (if session exists)
private String getUserType(HttpServletRequest request) { AuthorizedUser user = (AuthorizedUser) request.getSession().getAttribute("_user_session"); if (user == null) { return " "; } return user.getRole(); }
create new http session
/** * this will create a new http session for request that already contains * valid jwt cookie but previous session was expired, new session will * create for carry out user's further actions. */ private boolean createNewSession(HttpServletRequest request, String token) throws HibernateException, UnsupportedEncodingException { request.getSession().invalidate(); Object serviceResponse = getAuthenticationAndAuthorizationService().createSessionForUserAuthenticatedViaToken(token); if (serviceResponse instanceof SuccessServiceMessage) { AuthorizedUser au = ((AuthorizedUser) ((SuccessServiceMessage) serviceResponse).getPayload()); HttpSession user_session = request.getSession(); user_session.setAttribute("_user_session", au); user_session.setMaxInactiveInterval(3600); return true; } else { return false; } }
Full working code
@Autowired private JwtProviderService jwtProviderService; @Autowired private AuthenticationAndAuthorizationService authenticationAndAuthorizationService; /** * * @return */ private JwtProviderService getJwtProviderService() { return this.jwtProviderService; } /** * * @return */ private AuthenticationAndAuthorizationService getAuthenticationAndAuthorizationService() { return this.authenticationAndAuthorizationService; } @Override public void postHandle( HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView ) throws Exception { LOGGER.debug("INTERCEPTOR-Request post handler----------------------"); LOGGER.debug("INTERCEPTOR-Request path: {}", request.getServletPath()); LOGGER.debug("INTERCEPTOR-------------------------------------------"); } @Override public void afterCompletion( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex ) throws Exception { LOGGER.debug("INTERCEPTOR-Request completed-------------------------"); LOGGER.debug("INTERCEPTOR-Request path: {}", request.getServletPath()); LOGGER.debug("INTERCEPTOR-------------------------------------------"); } @Override public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler ) throws Exception { LOGGER.debug("INTERCEPTOR-A request has just hit----------------------"); LOGGER.debug("INTERCEPTOR-Request path: {}", request.getServletPath()); String header = request.getHeader("Authorization"); String request_url = request.getServletPath(); // Authorisation header not found - unauthorised, // redirects to another URL that creates appropriate response if (header == null || header.isEmpty()) { LOGGER.debug("INTERCEPTOR-UNAUTHORIZED authorization header not found"); response.sendRedirect( new StringBuilder() .append(request.getContextPath()) .append("/api/user/authentication") .append("?e=3") .toString() ); return false; } // Authorisation header not invalid - unauthorised // redirects to another URL that creates appropriate response if (!header.startsWith("Bearer ")) { LOGGER.debug("INTERCEPTOR-UNAUTHORIZED authorization header not found or malformed"); response.sendRedirect( new StringBuilder() .append(request.getContextPath()) .append("/api/user/authentication") .append("?e=3") .toString() ); return false; } String cleanedHeader = header.replace("Bearer ", "").trim(); // Authorisation header value expired/invalid - unauthorised // redirects to another URL that creates appropriate response if (!getJwtProviderService().validateToken(cleanedHeader)) { LOGGER.debug("INTERCEPTOR-UNAUTHORIZED authorization token is not valid or altered"); response.sendRedirect( new StringBuilder() .append(request.getContextPath()) .append("/api/user/authentication") .append("?e=1") .toString() ); return false; } /** * method 'authenticateUser(HttpServletRequest, String)' will do the * validation part of jwt and http session of this request. * * if jwt is successfully validated and the http session exists, and * these details are matched, it will return 'VIA_SESSION_AND_TOKEN' * enum. * * if jwt is successfully validated but http session has expired, then * it will return 'VIA_AUTHENTICATED_TOKEN' enum. * * if jwt is invalid, it will return 'NOT_AUTHENTICATED' enum. */ AuthenticationOption authenticateUser = getJwtProviderService().authenticateUser(request, cleanedHeader); // Authorisation header value expired/invalid/user not authenticated - unauthorised // redirects to another URL that creates appropriate response if (AuthenticationOption.NOT_AUTHENTICATED.equals(authenticateUser)) { LOGGER.debug("INTERCEPTOR-UNAUTHORIZED authorization token is not valid or altered"); response.sendRedirect( new StringBuilder() .append(request.getContextPath()) .append("/api/user/authentication") .append("?e=2") .toString() ); return false; } switch (authenticateUser) { case VIA_SESSION_AND_TOKEN: { // LOGGER.debug("INTERCEPTOR-AUTHORIZED via token and session"); String userType = getUserType(request); switch (userType) { case "ADMIN": return request_url.contains("/admin/") || request_url.contains("/api/") || request_url.contains("/user/"); case "USER": return request_url.contains("/user/") || request_url.contains("/api/"); default: return false; } } case VIA_AUTHENTICATED_TOKEN: { LOGGER.debug("INTERCEPTOR-AUTHORIZED via token"); //user has authorised via token, then creates new http session boolean create = createNewSession(request, cleanedHeader); //if new session has successfully created then re-directs //to same URL to execute authorization again. if (create) { response.sendRedirect( new StringBuilder() .append(request.getContextPath()) .append(request.getServletPath()) .toString() ); } } default: return false; } } /** * * @param request * @return */ private String getUserType(HttpServletRequest request) { AuthorizedUser user = (AuthorizedUser) request.getSession().getAttribute("_user_session"); if (user == null) { return " "; } return user.getRole(); } /** * * this will create a new http session for request that already contains * valid jwt cookie but previous session was expired, new session will * create for carry out user's further actions. */ private boolean createNewSession(HttpServletRequest request, String token) throws HibernateException, UnsupportedEncodingException { request.getSession().invalidate(); Object serviceResponse = getAuthenticationAndAuthorizationService().createSessionForUserAuthenticatedViaToken(token); if (serviceResponse instanceof SuccessServiceMessage) { AuthorizedUser au = ((AuthorizedUser) ((SuccessServiceMessage) serviceResponse).getPayload()); HttpSession user_session = request.getSession(); user_session.setAttribute("_user_session", au); user_session.setMaxInactiveInterval(3600); return true; } else { return false; } }