In this article, we will add authentication to our React Js app that we created in our last example. Basically, we will secure our REST APIs in the server-side and our private routes at the client-side. We will add a JWT token-based authentication and authorization in our app. The backend will be a spring boot project with spring security integrated and post-login a JWT token will be generated. Once, the token is generated, the client needs to produce that token in the request header to access all other secured resources.
We will have spring data integrated with the application to communicate with the DB as our user details will be saved in the database and the validation of the user credentials will be done from DB. Doing so, we can easily enable role-based authentication in our app.
In this tutorial, we will proceed step-by-step. At first, we will create a plain React Js app with material design integrated into it to perform CRUD operations on a User entity with backend APIs implemented in spring boot. In the next step, we will add spring security and JWT in our back-end app and then we will integrate this token-based authentication with our React app.
Few Words on JWT
JWT stands for JSON Web Token and is used for securely transmitting information between parties as a JSON object. JWT provides a stateless authentication mechanism as the user state is never saved in server memory. A JWT token consists of 3 parts separated with a dot(.) i.e. Header.payload.signature. Below is a sample JWT token:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBbGV4MTIzIiwic2N.v9A80eU1VDo2Mm9UqN2FyEpyT79IUmhg
If we decode this token, the result will be in the form of Header.payload.signature
JWT Header
JWT Header has 2 parts type of token and hashing algorithm used.The JSON structure comprising these two keys are Base64Encoded. The above token has below header.
{ "typ": "JWT", "alg": "HS256" }
JWT Payload
JWT Payload contains the claims.Primarily, there are three types of claims: reserved, public, and private claims. Reserved claims are predefined claims such as iss (issuer), exp (expiration time), sub (subject), aud (audience).In private claims, we can create some custom claims such as subject, role, and others. These custom claims provides stateless authentication mechanism.
For example, we have added actual role of the user in above JWT token and the sub contains the actual uername of the user.
{ "sub": "Alex123", "scopes": [ { "authority": "ROLE_ADMIN" } ], "iss": "http://devglan.com", "iat": 1508607322, "exp": 1508625322 }
JWT Signature
Signature provides the security to the JWT token and ensures that the token is not changed on the way. In case the token is tempered in the middle, the header and payload won't match with the signature and hence an error will be thrown while decoding the token.
There are many algorithms to encrypt the signature. For example, if you want to use the HMAC SHA256 algorithm, the signature will be created in the following way:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Now, let us understand how will we form our JWT token. While generating our custom JWT token, the Claim will be generated with actual user's username as subject and in the scopes of the Claim, we will have the user roles added. Below is the sample code for your understanding:
private String doGenerateToken(String subject) { Claims claims = Jwts.claims().setSubject(subject); claims.put("scopes", Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"))); return Jwts.builder() .setClaims(claims) .setIssuer("https://devglan.com") .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_SECONDS*1000)) .signWith(SignatureAlgorithm.HS256, SIGNING_KEY) .compact(); }
Creating React Js App
I assume the environment set up to build a React js app in your local machine is done already. Hence, execute below CLI commands to generate the boilerplate react app with create-react-app.
npx create-react-app react-js-example cd my-app npm start
This will install react, react-dom, react-scripts and all the third parties libraries that we need to get started with React app. It will install a lightweight development server, webpack for bundling the files and Babel for compiling our JS code. In my case, it is it's React Js version of 16.8.6, react-router-dom version of 5.0.1 and axios version of 0.19.0.
Now, let us add material designing to our app. For that, you can execute below CLI commands.
npm install @material-ui/core npm install @material-ui/icons
material-ui/core has all the core components related to Layout, Inputs, Navigation, etc. and with material-ui/icons, we can use all the prebuilt SVG Material icons.
REST APIs with Spring Boot
Once, we have our React app ready, let us create our backend system with spring boot to expose the REST APIs. For that, you can actually use https://start.spring.io project to download a sample project.
Maven Dependencies
Below is our pom.xml for this project. We are using a spring boot version 2.0.1.RELEASE with web and spring data JPA starter. We will include the security starter and JWT dependency later.
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> </dependencies> <properties> <java.version>1.8</java.version> </properties> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
Spring Boot Controller Implementation
The controller implementation is a very simple implementation to peform different HTTP operations such GET, POST, PUT, DELETE. The implementation is exactly the same that we created in our spring boot jwt article.
@CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @PostMapping public ApiResponsesaveUser(@RequestBody UserDto user){ return new ApiResponse<>(HttpStatus.OK.value(), "User saved successfully.",userService.save(user)); } @GetMapping public ApiResponse > listUser(){ return new ApiResponse<>(HttpStatus.OK.value(), "User list fetched successfully.",userService.findAll()); } @GetMapping("/{id}") public ApiResponse
getOne(@PathVariable int id){ return new ApiResponse<>(HttpStatus.OK.value(), "User fetched successfully.",userService.findById(id)); } @PutMapping("/{id}") public ApiResponse update(@RequestBody UserDto userDto) { return new ApiResponse<>(HttpStatus.OK.value(), "User updated successfully.",userService.update(userDto)); } @DeleteMapping("/{id}") public ApiResponse delete(@PathVariable int id) { userService.delete(id); return new ApiResponse<>(HttpStatus.OK.value(), "User deleted successfully.", null); } }
The corresponding service and DAO implementation source code can be found here and here.
Adding JWT to Backend App
To add JWT, let us first add the below maven dependencies in our pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
Now, let us add a util class to generate and validate our JWT token. The generateToken()
generates the token and to validate the token validateToken()
is invoked.
Component public class JwtTokenUtil implements Serializable { public String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); } public Date getExpirationDateFromToken(String token) { return getClaimFromToken(token, Claims::getExpiration); } publicT getClaimFromToken(String token, Function claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); } private Claims getAllClaimsFromToken(String token) { return Jwts.parser() .setSigningKey(SIGNING_KEY) .parseClaimsJws(token) .getBody(); } private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } public String generateToken(User user) { return doGenerateToken(user.getUsername()); } private String doGenerateToken(String subject) { Claims claims = Jwts.claims().setSubject(subject); claims.put("scopes", Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"))); return Jwts.builder() .setClaims(claims) .setIssuer("https://devglan.com") .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_SECONDS*1000)) .signWith(SignatureAlgorithm.HS256, SIGNING_KEY) .compact(); } public Boolean validateToken(String token, UserDetails userDetails) { final String username = getUsernameFromToken(token); return ( username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } }
Now, let us add our filter class that will intercept all the request coming to the server by extending OncePerRequestFilter.java.
This filter checks for any token present in the header and if the token is present then it will decode the JWT token. It will extract the username and roles and sets the spring security context.
If the token is not present in the header, the request will be chained to next filter in the filter chain and hence the spring security context will be empty. In this case, the spring security filter will not allow the request to reach to our secured controller layer and an unauthorized exception will be thrown if we try to access any secured resource.
JwtAuthenticationFilter.javapublic class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { String header = req.getHeader(HEADER_STRING); String username = null; String authToken = null; if (header != null && header.startsWith(TOKEN_PREFIX)) { authToken = header.replace(TOKEN_PREFIX,""); try { username = jwtTokenUtil.getUsernameFromToken(authToken); } catch (IllegalArgumentException e) { logger.error("an error occured during getting username from token", e); } catch (ExpiredJwtException e) { logger.warn("the token is expired and not valid anymore", e); } catch(SignatureException e){ logger.error("Authentication Failed. Username or Password not valid."); } } else { logger.warn("couldn't find bearer string, will ignore the header"); } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"))); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req)); logger.info("authenticated user " + username + ", setting security context"); SecurityContextHolder.getContext().setAuthentication(authentication); } } chain.doFilter(req, res); }
Spring Security Config
Now, let us define our security configuration. In the configuration, we have marked the endpoint matching with /token
as public and all the other APIs as secured. Hence, the React client must produce a token in the header to access the user resource. Here, we are using standard BCryptPasswordEncoder for encrypting our passwords.
We have added our custom filter before UsernamePasswordAuthenticationFilter and hence this filter will be invoked in the filter chain before UsernamePasswordAuthenticationFilter and the spring security context will be set. You can visit my previous article to know more about adding a custom filter in spring security filter chain.
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource(name = "userService") private UserDetailsService userDetailsService; @Autowired private JwtAuthenticationEntryPoint unauthorizedHandler; @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Autowired public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(encoder()); } @Bean public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception { return new JwtAuthenticationFilter(); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable(). authorizeRequests() .antMatchers("/token/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); } @Bean public BCryptPasswordEncoder encoder(){ return new BCryptPasswordEncoder(); } }
Generating JWT Token during Login
Now, let us add our login and logout method that will actually generate the JWT token during login and remove the token during logout.
@CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/token") public class AuthenticationController { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserService userService; @RequestMapping(value = "/generate-token", method = RequestMethod.POST) public ApiResponse<AuthToken> generateToken(@RequestBody LoginUser loginUser) throws AuthenticationException { authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword())); final User user = userService.findOne(loginUser.getUsername()); final String token = jwtTokenUtil.generateToken(user); return new ApiResponse<>(200, "success",new AuthToken(token, user.getUsername())); } @RequestMapping(value = "/logout", method = RequestMethod.POST) public ApiResponse<Void> logout() throws AuthenticationException { return new ApiResponse<>(200, "success",null); } }
In the login method, if the user provides any wrong credentials then the first line of this method will itself throw an exception with status cede as 401. Now, if you want to handle this and write your own exception handling mechanism in this case, then you can follow this article about exception handling in spring security.
The logout method does not do anything here as the APIs are stateless but we require this in some scenarios such as if we want to maintain a user to have a single session or some cleanup process when user logs out.
In our logout implementation in the client-side, we are actually not invoking this logout API as we do not have any particular use case.
React Js Components
Let us start implementing our React components. The different components that we have are login component, list user component, add user component, edit user component. But here, we will be only discussing about login component. All the other components are similar to my previous article here.
React Login Component
Login component has a material designed form that accepts username and password from the user. On the click of submit button, /generate-token
endpoint will be called which will validate the user credentials and retuns a token.
This token will be saved in the browser localstorage and it will be used to set the Authorization header from next time we inoke any API.
LoginComponent.jsimport React from 'react'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import TextField from '@material-ui/core/TextField'; import Button from '@material-ui/core/Button'; import Typography from '@material-ui/core/Typography'; import Container from '@material-ui/core/Container'; import AuthService from '../../service/AuthService'; class LoginComponent extends React.Component { constructor(props){ super(props); this.state = { username: '', password: '', message: '', } this.login = this.login.bind(this); } componentDidMount() { localStorage.clear(); } login = (e) => { e.preventDefault(); const credentials = {username: this.state.username, password: this.state.password}; AuthService.login(credentials).then(res => { if(res.data.status === 200){ localStorage.setItem("userInfo", JSON.stringify(res.data.result)); this.props.history.push('/list-user'); }else { this.setState({message: res.data.message}); } }); }; onChange = (e) => this.setState({ [e.target.name]: e.target.value }); render() { return( <React.Fragment> <AppBar position="static"> <Toolbar> <Typography variant="h6"> React User Application </Typography> </Toolbar> </AppBar> <Container maxWidth="sm"> <Typography variant="h4" style={styles.center}>Login</Typography> <form> <Typography variant="h4" style={styles.notification}>{this.state.message}</Typography> <TextField type="text" label="USERNAME" fullWidth margin="normal" name="username" value={this.state.username} onChange={this.onChange}/> <TextField type="password" label="PASSWORD" fullWidth margin="normal" name="password" value={this.state.password} onChange={this.onChange}/> <Button variant="contained" color="secondary" onClick={this.login}>Login</Button> </form> </Container> </React.Fragment> ) } } const styles= { center :{ display: 'flex', justifyContent: 'center' }, notification: { display: 'flex', justifyContent: 'center', color: '#dc3545' } } export default LoginComponent;
Below is the routing configuration.
const AppRouter = () => { return( <Router> <Switch> <Route path="/" exact component={LoginComponent} /> <Route path="/list-user" component={ListUserComponent} /> <Route path="/add-user" component={AddUserComponent} /> <Route path="/edit-user" component={EditUserComponent} /> </Switch> </Router> ) } export default AppRouter;
Adding JWT Token in React
To add JWT token in the header, we have a function defined in the auth service. Apart from login function, we have some util method defined here to get the header with JWT in it, get logged in user details, etc.
AuthService.jsimport axios from 'axios'; const USER_API_BASE_URL = 'http://localhost:8080/token/'; class AuthService { login(credentials){ return axios.post(USER_API_BASE_URL + "generate-token", credentials); } getUserInfo(){ return JSON.parse(localStorage.getItem("userInfo")); } getAuthHeader() { return {headers: {Authorization: 'Bearer ' + this.getUserInfo().token }}; } logOut() { localStorage.removeItem("userInfo"); return axios.post(USER_API_BASE_URL + 'logout', {}, this.getAuthHeader()); } } export default new AuthService();
We can also use jwt-decode library to decode JWT token in the client-side and different util methods can be implemented in this component.
JWT Role-Based Authorization
To add role-based authorization, we need to map each user with some roles in the DB. We can have Roles table in the DB and we can define many-to-many mappings between Users and Roles.
During login, once the user is validated, we can fetch the corresponding roles from the DB and tweak the generateToken()
defined in the JwtTokenUtil.java to accept a list of roles to generate the token.
public String generateToken(User user, List<Role> roles) { return doGenerateToken(user.getUsername(), roles); } private String doGenerateToken(String subject, List<Role> roles) { Claims claims = Jwts.claims().setSubject(subject); claims.put("scopes", roles.stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList())); return Jwts.builder() .setClaims(claims) .setIssuer("https://devglan.com") .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_SECONDS*1000)) .signWith(SignatureAlgorithm.HS256, SIGNING_KEY) .compact(); }
Now, we can annotate our controller methods to have role based authorization.
//@Secured("ROLE_USER") //@PreAuthorize("hasRole('USER')") @PreAuthorize("hasAnyRole('USER', 'ADMIN')") @RequestMapping(value = "/user/{id}", method = RequestMethod.GET) public User getOne(@PathVariable(value = "id") Long id){ return userService.findById(id); }
You can find the complete implementation for role based auth here.
Adding CORS Filter
To unblock all the cross origin request from the browser to our server, we can add below filter in the spring app. Actually, it sets the header Access-Control-Allow-Origin
to *
allowing request from any domain but we can definitely restrict it the domain of your choice.
public class CORSFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { System.out.println("Filtering on..........................................................."); HttpServletResponse response = (HttpServletResponse) res; response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Authorization, Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers"); chain.doFilter(req, res); } public void init(FilterConfig filterConfig) {} public void destroy() {} }
Conclusion
In this article, we added authentication to our React Js app. We secured our REST APIs in the server-side and our private routes at the client-side. We added a JWT token-based authentication and authorization in our app. You can download the source from here.