Adding JWT Authentication in React Js (with Spring Security)

Adding JWT Authentication in React Js (with Spring Security) thumbnail
62K
By Dhiraj 31 July, 2019

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 ApiResponse saveUser(@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.

JwtTokenUtil.java
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);
    }

    public  T 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.java
public 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.js
import 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;
reactjs-spring-boot-jwt-login

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.js
import 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.

Share

If You Appreciate This, You Can Consider:

We are thankful for your never ending support.

About The Author

author-image
A technology savvy professional with an exceptional capacity to analyze, solve problems and multi-task. Technical expertise in highly scalable distributed systems, self-healing systems, and service-oriented architecture. Technical Skills: Java/J2EE, Spring, Hibernate, Reactive Programming, Microservices, Hystrix, Rest APIs, Java 8, Kafka, Kibana, Elasticsearch, etc.

Further Reading on React JS