Skip to content

3 Token Based Authentication

3 Token-Based Authentication

Goal

  • Brief introduction of token-based authentication.
  • Implement user signin functions.

Screenshot 2024-07-09 at 19.26.43

Token-Based vs Session-Based Authentication

sequenceDiagram
    participant Browser
    participant Server

    rect rgb(191, 223, 255)
    note right of Browser: Session-Based Auth
    Browser->>Server: POST /authenticate username=_&password=_
    Server-->>Browser: HTTP 200 OK Set-Cookie: session=...
    Browser->>Server: GET /api/user Cookie: session=...
    Server-->>Browser: HTTP 200 OK {name: "foo"}
    end

Session-Based Authentication

How it works

  1. A user enters their login credentials (username + password).
  2. The server verifies the credentials are correct and creates a session which is then stored in a database.
  3. The client-side stores the session ID returned from the server.
  4. On subsequent requests, the session ID is verified against the database and if valid the request is processed.
  5. Once a user logs out of the app, the session is destroyed on the server-side.
sequenceDiagram
    participant Browser
    participant Server

    rect rgb(220, 220, 220)
    note right of Browser: Token-Based Auth
    Browser->>Server: POST /authenticate username=_&password=_
    Server-->>Browser: HTTP 200 OK {token: "JWT..."}
    Browser->>Server: GET /api/user Authorization: Bearer ...JWT
    Server-->>Browser: HTTP 200 OK {name: "foo"}
    end

Token-Based Authentication

How it works

  1. A user enters their login credentials (=username + password).
  2. The server verifies the credentials are correct and creates an encrypted and signed token with a private key ( { username: “abcd”, exp: “2021/1/1/10:00” }, private key => token).
  3. The client-side stores the token returned from the server.
  4. On subsequent requests, the token is decoded with the same private key and if valid the request is processed.
  5. Once a user logs out, the token is destroyed client-side, no interaction with the server is necessary.

Advantages of Token-Based Authentication

Stateless, Scalable and Decoupled

  • Stateless: The back-end does not need to keep a record of tokens.
  • Self-contained, containing all the data required to check its validity. No DB lookup is needed.

Mobile Friendly

  • Native mobile platforms and cookies do not mix well.

Disadvantages of Token-Based Authentication

  • The size of a token is usually larger than a session id.

Session-Based Authentication 和 Token-Based Authentication 的主要区别如下:

Session-Based Authentication

  1. 会话创建:用户登录后,服务器会在服务器端创建一个会话(session),并在响应中通过 Set-Cookie 头将会话 ID 发送给客户端。
  2. 会话存储:服务器在内存或数据库中保存会话信息,客户端每次请求时都会发送这个会话 ID。
  3. 会话验证:服务器通过会话 ID 查找并验证用户的会话。
  4. 状态管理:服务器负责管理会话状态,因此扩展性较差,因为需要在服务器上维护所有用户的会话信息。

Token-Based Authentication

  1. 令牌生成:用户登录后,服务器生成一个 JWT(JSON Web Token),并在响应中返回给客户端。
  2. 令牌存储:客户端(通常是浏览器)将这个令牌存储在本地(如 localStorage 或 sessionStorage)。
  3. 令牌验证:客户端每次请求时都会在 Authorization 头中发送这个令牌,服务器验证令牌的有效性。
  4. 无状态管理:服务器不存储任何会话信息,仅验证令牌,因此扩展性较好,因为服务器无需保存用户的状态信息。

主要区别

  1. 存储方式: - Session-Based:会话信息存储在服务器上。 - Token-Based:令牌存储在客户端上。
  2. 验证方式: - Session-Based:服务器验证会话 ID。 - Token-Based:服务器验证令牌的签名和有效性。
  3. 扩展性: - Session-Based:服务器需要存储和管理所有会话信息,扩展性较差。 - Token-Based:服务器不存储会话信息,仅验证令牌,扩展性较好。
  4. 安全性: - Session-Based:容易受到 CSRF 攻击,但可以通过使用 SameSite 属性和 CSRF 令牌来防护。 - Token-Based:更容易受到 XSS 攻击,因为令牌存储在客户端的脚本可访问的位置,但可以通过使用 HttpOnly 属性来减轻风险。

Implement Authentication with JSON Web Token(JWT)

Download JWT Library

  1. Open your staybooking project and navigate to the pom.xml file. Add the following dependency.
    <dependencies>

  <!-- ... existing dependencies -->

  <!-- Only insert the following, do not change or touch other lines -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
  <!-- Only insert the above, do not change or touch other lines -->
</dependencies>
  1. Save your changes in pom.xml and run Maven Install to download the java libraries. Make sure you see the BUILD SUCCESS information from the console output.

Screenshot 2024-07-10 at 15.43.10

Enable Spring Security AuthenticationManager

  1. Need to configure the AuthenticationManager provided by Spring Security under the SecurityConfig.class to read user/authority data from the MySQL database for authentication.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import javax.sql.DataSource;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        ...
    }
//https://en.wikipedia.org/wiki/SQL_injection
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication().dataSource(dataSource)
                .passwordEncoder(passwordEncoder())
                .usersByUsernameQuery("SELECT username, password, enabled FROM user WHERE username = ?")
                .authoritiesByUsernameQuery("SELECT username, authority FROM authority WHERE username = ?");
    }
}
  1. In addition to configuring the datasource for AuthenticationManage, we also need to expose it as a bean so that we can use it in our authentication service. In the session-based authentication, we don't do it becasue we can use http.loginForm() provided by Spring Security.
import org.springframework.security.authentication.AuthenticationManager;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        ...
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        ...
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

Explanation

  1. PasswordEncoder Bean: Configures the BCryptPasswordEncoder.
  2. SecurityFilterChain Bean: Configures the HTTP security, allowing POST requests to /register/* and requiring authentication for other requests.
  3. AuthenticationManager Bean: Configures the AuthenticationManager with JDBC authentication, password encoding, and SQL queries for user details and authorities.

This setup avoids using WebSecurityConfigurerAdapter and properly configures Spring Security with modern practices.

Screenshot 2024-07-10 at 16.36.31

  1. Create a new Token class under com.eve.staybooking.model package. We don’t need to mark the class as @Entity since we don’t store token information in database.
package com.eve.staybooking.model;

public class Token {
    private final String token;

    public Token(String token) {
        this.token = token;
    }

    public String getToken() {
        return token;
    }
}

Screenshot 2024-07-10 at 16.38.13

  1. Create a new package called com.eve.staybooking.util and add a new class JwtUtil to it.
package com.eve.staybooking.util;

public class JwtUtil {
}

Screenshot 2024-07-10 at 16.39.20

  1. Open the application.properties file and add a new variable named jwt.secret. We’ll use the value of the jwt.secret for JWT generation. You can use any string as the value.
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql = true
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
spring.datasource.url = jdbc:mysql://localhost:3306/staybooking?createDatabaseIfNotExist=true&serverTimezone=UTC
spring.datasource.username = root
spring.datasource.password = Eve123456
jwt.secret=secret

Screenshot 2024-07-10 at 16.41.15

  1. Go back to the JwtUtil.class, add a private field secretKey.
package com.eve.staybooking.util;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class JwtUtil {
    @Value("${jwt.secret}")
    private String secret;
}

Screenshot 2024-07-10 at 16.42.14

  1. Add a method to generate the JWT and return the encrypted result of it.
package com.eve.staybooking.util;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;

@Component
public class JwtUtil {
    @Value("${jwt.secret}")
    private String secret;

    public String generateToken(String subject) {
        return Jwts.builder()
                .setClaims(new HashMap<>())
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();
    }

}

Screenshot 2024-07-10 at 16.43.05

  1. Next to the generateToken() method, add methods to decrypt a JWT from the encrypted value.

    import io.jsonwebtoken.Claims;
    
    @Component
    public class JwtUtil {
        @Value("${jwt.secret}")
        private String secret;
    
        public String generateToken(String subject) {
            ...
        }
    
        private Claims extractClaims(String token) {
            return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        }
    
        public String extractUsername(String token) {
            return extractClaims(token).getSubject();
        }
    
        public Date extractExpiration(String token) {
            return extractClaims(token).getExpiration();
        }
    
        public Boolean validateToken(String token) {
            return extractExpiration(token).after(new Date());
        }
    }
    

Create Authentication Service

  1. Go to com.eve.staybooking.service package and create a new class AuthenticationService.
package com.eve.staybooking.service;

import org.springframework.stereotype.Service;

@Service
public class AuthenticationService {

}
  1. Add the AuthenticationManager, AuthorityRepository, and JwtUtil as the private field and create a constructor for initialization.
package com.eve.staybooking.service;

import com.eve.staybooking.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.stereotype.Service;

@Service
public class AuthenticationService {
    private AuthenticationManager authenticationManager;
    private JwtUtil jwtUtil;

    @Autowired
    public AuthenticationService(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
        this.authenticationManager = authenticationManager;
        this.jwtUtil = jwtUtil;
    }
}
  1. Similar to the RegisterService, we should throw a UserNotExist exception when the given user credential is invalid. So go to the com.eve.staybooking.exception package and create a UserNotExistException.class.
package com.eve.staybooking.exception;

public class UserNotExistException extends RuntimeException{
    public UserNotExistException(String message) {
        super(message);
    }
}
  1. Go back to the AuthenticationService and add the authenticate method to check the user credential and return the Token if everything is OK.
package com.eve.staybooking.service;

import com.eve.staybooking.exception.UserNotExistException;
import com.eve.staybooking.model.Token;
import com.eve.staybooking.model.User;
import com.eve.staybooking.model.UserRole;
import com.eve.staybooking.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;

@Service
public class AuthenticationService {
    private AuthenticationManager authenticationManager;
    private JwtUtil jwtUtil;

    @Autowired
    public AuthenticationService(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
        this.authenticationManager = authenticationManager;
        this.jwtUtil = jwtUtil;
    }

    public Token authenticate(User user, UserRole role) throws UserNotExistException {
        Authentication auth = null;
        try {
            auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
        } catch (Exception e) {
            throw new UserNotExistException("User does not exist");

        }

        if (auth == null || !auth.isAuthenticated() || !auth.getAuthorities().contains(new SimpleGrantedAuthority(role.name()))) {
            throw new UserNotExistException("User Doesn't Exist");
        }
        return new Token(jwtUtil.generateToken(user.getUsername()));

    }
}

Screenshot 2024-07-10 at 17.25.42

Create Authentication Controller

  1. Go to com.eve.staybooking.controller package and create a new class AuthenticationController.
package com.eve.staybooking.controller;

import org.springframework.web.bind.annotation.RestController;

@RestController
public class AuthenticationController {
}
  1. Add AuthenticationService as a private field and implement two separate authentication APIs for host and guest.
package com.eve.staybooking.controller;

import com.eve.staybooking.model.Token;
import com.eve.staybooking.model.User;
import com.eve.staybooking.model.UserRole;
import com.eve.staybooking.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AuthenticationController {
    private AuthenticationService authenticationService;

    @Autowired
    public AuthenticationController(AuthenticationService authenticationService) {
        this.authenticationService = authenticationService;
    }

    @PostMapping("/authenticate/guest")
    public Token authenticateGuest(@RequestBody User user) {
        return authenticationService.authenticate(user, UserRole.ROLE_GUEST);
    }

    @PostMapping("/authenticate/host")
    public Token authenticateHost(@RequestBody User user) {
        return authenticationService.authenticate(user, UserRole.ROLE_HOST);
    }
}

Screenshot 2024-07-10 at 17.27.54

  1. Go to CustomExceptionHandler and add a new exception handler for UserNotExistException.
package com.eve.staybooking.controller;

import com.eve.staybooking.exception.UserAlreadyExistException;
import com.eve.staybooking.exception.UserNotExistException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

@ControllerAdvice
public class CustomExceptionHandler {
    @ExceptionHandler(UserAlreadyExistException.class)
    public final ResponseEntity<String> handleUserAlreadyExistExceptions(Exception ex, WebRequest request){
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.CONFLICT);
    }

    @ExceptionHandler(UserNotExistException.class)
    public final ResponseEntity<String> handleUserNotExistExceptions(Exception ex, WebRequest request){
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.UNAUTHORIZED);
    }

}
  1. Update SecurityConfig to allow /authentcation/guest and /authenticate/host URLs.

Screenshot 2024-07-10 at 17.29.49

Test

  1. Save all your changes and start your project. Make sure there’s no error in the log.

Screenshot 2024-07-10 at 17.31.02

  1. Use the sample request in the Postman collection to verify both guest and host authentication are OK. You should see the token string in the response.

Screenshot 2024-07-10 at 17.59.26

Screenshot 2024-07-10 at 17.59.34

java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.1</version>
</dependency>

Screenshot 2024-07-10 at 17.59.42