Contents
RESTful API Authentication Method
Generally speaking, RESTful API get Authentication and Authorization to make sure the safety of the API.
Authentication vs Authorization
Authentication means the identity of the user and Authorization means what operation rights the user has.
Ways Of Authentication
-
Basic Authentication This means put the username and password directly into the HTTP request header. This is the simplest way but not recommended.
-
TOKEN Authentication This is most commonly used way that put JWT TOKEN directly into the HTTP request header. This is the recommended way.
-
OAuth2.0 This is the most secure way but also the most complicated way. If not necessary don’t consider this way.
Normally we just use JWT for Authentication in our projects.
What Is JWT
JSON Web Token (JWT, sometimes pronounced) is a JSON-based open standard (RFC 7519) for creating access tokens that assert some number of claims. For example, a server could generate a token that has the claim “logged in as admin” and provide that to a client. The client could then use that token to prove that it is logged in as admin. The tokens are signed by one party’s private key (usually the server’s), so that both parties (the other already being, by some suitable and trustworthy means, in possession of the corresponding public key) are able to verify that the token is legitimate. The tokens are designed to be compact, URL-safe, and usable especially in a web-browser single-sign-on (SSO) context. JWT claims can be typically used to pass the identity of authenticated users between an identity provider and a service provider, or any other type of claims as required by business processes. Official Web Site: https://jwt.io/ JWT is combined with three parts, all these parts together formed JWS string like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ.
Three Parts Of JWT
- Header
- Payload
- Signature
Header
Header has two parts:
- Declarition Type usually JWT
- Declaring an encrypted algorithm Typical cryptographic algorithms used are HMAC with SHA-256 (HS256) and RSA signature with SHA-256 (RS256). JWA (JSON Web Algorithms) RFC 7518 introduces many more for both authentication and encryption.
The header usually looks like this:
{ 'typ': 'JWT', 'alg': 'HS256' }
then encryt the header turn it into this:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Payload
The payload contains three parts:
- Registered Claim
- Public Claim
- Private Claim
Registered Claim
code | name | description |
---|---|---|
iss |
Issuer | Identifies principal that issued the JWT. |
sub |
Subject | Identifies the subject of the JWT. |
aud |
Audience | Identifies the recipients that the JWT is intended for. Each principal intended to process the JWT must identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the aud claim when this claim is present, then the JWT must be rejected. |
exp |
Expiration Time | Identifies the expiration time on and after which the JWT must not be accepted for processing. The value must be a NumericDate[10]: either an integer or decimal, representing seconds past 1970-01-01 00:00:00Z. |
nbf |
Not Before | Identifies the time on which the JWT will start to be accepted for processing. The value must be a NumericDate. |
iat |
Issued at | Identifies the time at which the JWT was issued. The value must be a NumericDate. |
jti |
JWT ID | Case sensitive unique identifier of the token even among different issuers. |
Public Claim
Generally speaking, the public claim can contain any information but it is not recommended to add sensitive information here since it can be easily decrypted.
Private Claim
Private Claim is the claimed by both client side and server side. Also, sensitive information is not recommended claimed here.
A classical payload looks like this:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
After based64 encrypt we get the second part of JWT:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
Signature
Signature contains three parts:
- header (after encrypted)
- payload (after encrypted)
- secret
The third part of JWT is the combination of the component i mentioned before:
header(after encrypted)+payload(after encrypted)+secret
After encrypt get finally got our JWT string:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
Notice that secret is kept on the server side and should never be leaked.
How Could We Use JWT In Our Application
Generally speaking we add these information in our HTTP request header:
fetch('api/user/1', { headers: { 'Authorization': 'Bearer ' + token } })
The server is responsible for analysis the HTTP header for Authentication and Authorization as demonstrate below:
Security Related Issue
The JWT protocol itself does not have a secure transport function, so it must rely on the secure channel of SSL/TLS, so the recommendations are as follows:
- Sensitive information should not be stored in the payload portion of jwt, as this part is the part that the client can decrypt.
- Protect the secret private key, which is very important.
- If you can, please use the HTTPS protocol
Integrate With SpringBoot
Since we want to achieve complete front-end & back-end separation, so it is impossible to use session, cookie way for authentication thus JWT is used.
Program Logic
-
Send POST request to /login if successed return a TOKEN if failed return
UnauthorizedException
-
After the user accesses each URL request that requires permission, the Authorization field must be added to the header, such as
Authorization: token
, and token is the key. -
Back-end will authenticate the request otherwise 401
Our security framework is :
-
Apche Shiro
-
java-jwt
Add Maven Dependency
<dependencies> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.16</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>1.5.8.RELEASE</version> </dependency> </dependencies>
Build Mock Datasource
Inorder to focus on how to ues JWT not the DB I fake a Datasource as below:
| username | password | role | permission |
| -------- | -------- | ----- | ---------- |
| smith | smith123 | user | view |
| danny | danny123 | admin | view,edit |
Then next build a UserService
to mock DB query and put the result into the UserBean
.
UserService.java
@Component public class UserService { public UserBean getUser(String username) { // If no such user return null if (! DataSource.getData().containsKey(username)) return null; UserBean user = new UserBean(); Map<String, String> detail = DataSource.getData().get(username); user.setUsername(username); user.setPassword(detail.get("password")); user.setRole(detail.get("role")); user.setPermission(detail.get("permission")); return user; } }
UserBean.java
public class UserBean { private String username; private String password; private String role; private String permission; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } public String getPermission() { return permission; } public void setPermission(String permission) { this.permission = permission; } }
Config JWT
We build a simple JWT enrypt tool and make user password as encrypt password thus make sure even if you token is stolen by others they cannot hack it.And besides, we put username in our TOKEN ,then TOKEN will be expired 5 minutes later.
public class JWTUtil { // Expired 5 minutes later private static final long EXPIRE_TIME = 5*60*1000; /** * Verify TOKEN * @param token * @param secret User password * @return */ public static boolean verify(String token, String username, String secret) { try { Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("username", username) .build(); DecodedJWT jwt = verifier.verify(token); return true; } catch (Exception exception) { return false; } } /** * Get username from TOKEN * @return token contains username information */ public static String getUsername(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } catch (JWTDecodeException e) { return null; } } /** * generate signature, the signature will be expired 5 minutes later * @param username * @param secret * @return Encryted token */ public static String sign(String username, String secret) { try { Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(secret); return JWT.create() .withClaim("username", username) .withExpiresAt(date) .sign(algorithm); } catch (UnsupportedEncodingException e) { return null; } } }
ResponseBean.java
public class ResponseBean { // http status code private int code; // return message private String msg; // return data private Object data; public ResponseBean(int code, String msg, Object data) { this.code = code; this.msg = msg; this.data = data; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } }
Custom Exception
public class UnauthorizedException extends RuntimeException { public UnauthorizedException(String msg) { super(msg); } public UnauthorizedException() { super(); } }
URL
URL | Function |
---|---|
/login | Login |
/article | Everyone can visit but different role will get different content |
/require_auth | Login user can visit |
/require_role | admin role can visit |
/require_permission | view and edit role can visit |
Controller
@RestController public class WebController { private static final Logger LOGGER = LogManager.getLogger(WebController.class); private UserService userService; @Autowired public void setService(UserService userService) { this.userService = userService; } @PostMapping("/login") public ResponseBean login(@RequestParam("username") String username, @RequestParam("password") String password) { UserBean userBean = userService.getUser(username); if (userBean.getPassword().equals(password)) { return new ResponseBean(200, "Login success", JWTUtil.sign(username, password)); } else { throw new UnauthorizedException(); } } @GetMapping("/article") public ResponseBean article() { Subject subject = SecurityUtils.getSubject(); if (subject.isAuthenticated()) { return new ResponseBean(200, "You are already logged in", null); } else { return new ResponseBean(200, "You are guest", null); } } @GetMapping("/require_auth") @RequiresAuthentication public ResponseBean requireAuth() { return new ResponseBean(200, "You are authenticated", null); } @GetMapping("/require_role") @RequiresRoles("admin") public ResponseBean requireRole() { return new ResponseBean(200, "You are visiting require_role", null); } @GetMapping("/require_permission") @RequiresPermissions(logical = Logical.AND, value = {"view", "edit"}) public ResponseBean requirePermission() { return new ResponseBean(200, "You are visiting permission require edit,view", null); } @RequestMapping(path = "/401") @ResponseStatus(HttpStatus.UNAUTHORIZED) public ResponseBean unauthorized() { return new ResponseBean(401, "Unauthorized", null); } }
Deal With The Exception
As mentioned before that RESTFUL need uniform style so we need to deal with the Spring Boot exception.
We can use @RestControllerAdvice
to do this.
@RestControllerAdvice public class ExceptionController { // Catch Shiro Exception @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(ShiroException.class) public ResponseBean handle401(ShiroException e) { return new ResponseBean(401, e.getMessage(), null); } // Catch UnauthorizedException @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(UnauthorizedException.class) public ResponseBean handle401() { return new ResponseBean(401, "Unauthorized", null); } // Catch Other Exception @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ResponseBean globalException(HttpServletRequest request, Throwable ex) { return new ResponseBean(getStatus(request).value(), ex.getMessage(), null); } private HttpStatus getStatus(HttpServletRequest request) { Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code"); if (statusCode == null) { return HttpStatus.INTERNAL_SERVER_ERROR; } return HttpStatus.valueOf(statusCode); } }
Config Shiro
Achive JWTToken
public class JWTToken implements AuthenticationToken { // TOKEN private String token; public JWTToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
Realm
@Service public class MyRealm extends AuthorizingRealm { private static final Logger LOGGER = LogManager.getLogger(MyRealm.class); private UserService userService; @Autowired public void setUserService(UserService userService) { this.userService = userService; } @Override public boolean supports(AuthenticationToken token) { return token instanceof JWTToken; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = JWTUtil.getUsername(principals.toString()); UserBean user = userService.getUser(username); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.addRole(user.getRole()); Set<String> permission = new HashSet<>(Arrays.asList(user.getPermission().split(","))); simpleAuthorizationInfo.addStringPermissions(permission); return simpleAuthorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { String token = (String) auth.getCredentials(); String username = JWTUtil.getUsername(token); if (username == null) { throw new AuthenticationException("token invalid"); } UserBean userBean = userService.getUser(username); if (userBean == null) { throw new AuthenticationException("User didn't existed!"); } if (! JWTUtil.verify(token, username, userBean.getPassword())) { throw new AuthenticationException("Username or password error"); } return new SimpleAuthenticationInfo(token, token, "my_realm"); } }
Define Filter
All the request will forward to Filter
,we extends BasicHttpAuthenticationFilter
to override some methods
execution flow :preHandle
->isAccessAllowed
->isLoginAttempt
->executeLogin
public class JWTFilter extends BasicHttpAuthenticationFilter { private Logger LOGGER = LoggerFactory.getLogger(this.getClass()); @Override protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { HttpServletRequest req = (HttpServletRequest) request; String authorization = req.getHeader("Authorization"); return authorization != null; } @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String authorization = httpServletRequest.getHeader("Authorization"); JWTToken token = new JWTToken(authorization); getSubject(request, response).login(token); return true; } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (isLoginAttempt(request, response)) { try { executeLogin(request, response); } catch (Exception e) { response401(request, response); } } return true; } @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } /** * Illege request foward to /401 */ private void response401(ServletRequest req, ServletResponse resp) { try { HttpServletResponse httpServletResponse = (HttpServletResponse) resp; httpServletResponse.sendRedirect("/401"); } catch (IOException e) { LOGGER.error(e.getMessage()); } } }
Config Shiro
@Configuration public class ShiroConfig { @Bean("securityManager") public DefaultWebSecurityManager getManager(MyRealm realm) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(realm); DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); manager.setSubjectDAO(subjectDAO); return manager; } @Bean("shiroFilter") public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); // define your filter and name it as jwt Map<String, Filter> filterMap = new HashMap<>(); filterMap.put("jwt", new JWTFilter()); factoryBean.setFilters(filterMap); factoryBean.setSecurityManager(securityManager); factoryBean.setUnauthorizedUrl("/401"); /* * difine custom URL rule * http://shiro.apache.org/web.html#urls- */ Map<String, String> filterRuleMap = new HashMap<>(); // All the request forword to JWT Filter filterRuleMap.put("/**", "jwt"); // 401 and 404 page does not forward to our filter filterRuleMap.put("/401", "anon"); factoryBean.setFilterChainDefinitionMap(filterRuleMap); return factoryBean; } @Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
Run It
Get TOKEN by sending POST request
Add TOKEN in the HTTP request header and get the result
If without TOKEN or the wrong TOKEN you will get the following error:
Sunil
24 Feb 2019Very Nice article. Just what i was looking for. But i have one question in this. How to implement logout? If i just say SecurityUtils.getSubject().logout(); it does not work because the JWT token might be still valid.
kobe73er
7 Mar 2019Glad you like this post.
I think for your question this may be a good answer:
https://stackoverflow.com/questions/43569723/jwt-authentication-how-to-implement-logout
rhuka92
13 May 2019Thanks a lot.
This was very helpful!
~ Starred your repo ~
kobe73er
4 Oct 2019Thanks!
Michail
9 Jun 2019Very Nice Article. However i am facing some problems. I am trying to adapt your code in my situation where i don’t use Spring Boot But Java EE Jersey to build a Restful Service and Shiro for Authentication Authorization. However i get an error in preHandle function of JWTFilter
RequestMethod and HttpStatus are not found. Am i missing any imports?
Can i just comment out whole preHandle function?
What exactly the if statement inside preHandle function of JWTFilter does??
Thanks a lot in advance