DEV Community

Naman Tamrakar
Naman Tamrakar

Posted on

JWT Token authorisation in spring boot 3 without any external library

In this blog post, we'll walk through a Spring Boot application that sets up a secure environment using JWT authentication. In most of the blogs or tutorials you must have implemented jwt authentication using a 3rd party library, But for this implement we would use spring-oauth2-authorization-server which is provided by spring framework itself. So without wasting much time let's start.

So before starting coding let's head over to start.spring.io to get our project ready. We will mainly add

  1. Spring web (For setting up servlet)
  2. Spring data jdbc and h2 (For database and ORM)
  3. Spring oauth2 authorization server (For JWT authorisation)
  4. Lombok (To reduce boiler plate code)

Our dependencies should look like this:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Next we will create a protected REST endpoint to test our API.

class HomeController {
    @GetMapping
    String home() {
        return "home";
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating user model

import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.security.core.userdetails.UserDetails;

@ToString
@Table("USERS") // using custom table name as user keyword reserved
class User implements UserDetails {
    @Id
    Long id;
    String username;
    String password;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of();
    }
    @Override
    public String getPassword() {
        return password;
    }
    @Override
    public String getUsername() {
        return username;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating user repository

import org.springframework.data.repository.CrudRepository;
interface UserRepository extends CrudRepository<User, Long> {
    Optional<User> findByUsername(String username);
}
Enter fullscreen mode Exit fullscreen mode

Now setting up our token authorisation

For signing our jwt token we will create RSA private/public key pair using openssl tool.

# run these command in resources/certs directory
# create rsa key pair
openssl genrsa -out keypair.pem 2048
# extract public key
openssl rsa -in keypair.pem -pubout -out public.pem
# create private key in PKCS#8 format
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out private.pem
Enter fullscreen mode Exit fullscreen mode

Once we have our key in place add the following propertied in application.properties file.

rsa.private-key=classpath:certs/private.pem
rsa.public-key=classpath:certs/public.pem
# for logging add the properties
logging.level.org.springframework.security=TRACE
logging.level.org.springframework.jdbc=TRACE
server.error.include-message=always
Enter fullscreen mode Exit fullscreen mode

Now add this security config

@EnableWebSecurity
@Configuration
class SecurityConfig {
    private @Value("${rsa.private-key}") RSAPrivateKey privateKey;
    private @Value("${rsa.public-key}") RSAPublicKey publicKey;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/auth/**").permitAll()
                        .anyRequest().authenticated()
                )
                .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .build();
    }
    @Bean
    public UserDetailsService userDetailsService(UserRepository userRepository) {
        return username -> userRepository
                .findByUsername(username)
                .orElseThrow(() ->
                        new UsernameNotFoundException("user with username " + username + " not found!!")
                );
    }
    @Bean
    JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withPublicKey(publicKey).build();
    }
    @Bean
    JwtEncoder jwtEncoder() {
        JWK jwk = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
        JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
        return new NimbusJwtEncoder(jwks);
    }
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }
}
Enter fullscreen mode Exit fullscreen mode

To generate the token for logging in add a token generation service

@RequiredArgsConstructor
@Service
class TokenService {
    private final JwtEncoder encoder;
    public String generateToken(Authentication authentication) {
        Instant now = Instant.now();
        String scope = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(" "));
        JwtClaimsSet claims = JwtClaimsSet.builder()
                .issuer("self")
                .issuedAt(now)
                .expiresAt(now.plus(1, ChronoUnit.HOURS))
                .subject(authentication.getName())
                .claim("scope", scope)
                .build();
        return this.encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
    }
}
Enter fullscreen mode Exit fullscreen mode

Adding an endpoint for logging in our controller

@PostMapping("/auth/login")
public String login(@RequestBody Map<String, String> user) {
    log.debug("User login: {}", user);
    Authentication auth = manager.authenticate(
            new UsernamePasswordAuthenticationToken(user.get("username"), user.get("password")));
    String token = tokenService.generateToken(auth);
    log.debug("JWT token: {}", token);
    return token;
}
Enter fullscreen mode Exit fullscreen mode

And to create user for initial startup add this bean

@Bean
ApplicationRunner runner(UserRepository userRepository, PasswordEncoder encoder) {
    return args -> {
        User user = new User();
        user.username = "admin";
        user.password = encoder.encode("admin");
        var savedUser = userRepository.save(user);
        log.info("user: {}", savedUser);
    };
}
Enter fullscreen mode Exit fullscreen mode

Now let's test our API

> curl -X POST http://localhost:8080/auth/login -d '{"username":"admin", "password": "admin"}' -H "Content-type: application/json"
Enter fullscreen mode Exit fullscreen mode

output

eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzZWxmIiwic3ViIjoiYWRtaW4iLCJleHAiOjE3MjIwMzUyOTksImlhdCI6MTcyMjAzMTY5OSwic2NvcGUiOiIifQ.SwRW7_P4xMp5-aLTcUYZyL9jncTIVHUFswaQoKupwfhaoAWYRpmP5g-Ras1gU2oAJ_saY7jt0N-WiezS977FAgDOYYmdxr2H4SBeg0qNqsSV6fOX9zRELYukdbEnT9GGaAk5u9WXd6JoHa5x_7vqXJ1Uu2ciDNxgRaBVtwoeWawZ53g0EFwjqturuQALWfBkkLafMbkY_dSqAoohmvTlVVGWiYPylGOsIhQ3CjamZVzqq8ma9WHUPx8p8luz7EBdsjm1MQYRrtC3NiH2TJaQqJTQenevZ8_YoKUUk6VddVzAxc0xycnmY-g94zEVJyjdKeOM98AjEE9C4oX5XgGuKA
Enter fullscreen mode Exit fullscreen mode

and now test our protected enpoint with jwt token

> curl http://localhost:8080 -H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzZWxmIiwic3ViIjoiYWRtaW4iLCJleHAiOjE3MjIwMzUyOTksImlhdCI6MTcyMjAzMTY5OSwic2NvcGUiOiIifQ.SwRW7_P4xMp5-aLTcUYZyL9jncTIVHUFswaQoKupwfhaoAWYRpmP5g-Ras1gU2oAJ_saY7jt0N-WiezS977FAgDOYYmdxr2H4SBeg0qNqsSV6fOX9zRELYukdbEnT9GGaAk5u9WXd6JoHa5x_7vqXJ1Uu2ciDNxgRaBVtwoeWawZ53g0EFwjqturuQALWfBkkLafMbkY_dSqAoohmvTlVVGWiYPylGOsIhQ3CjamZVzqq8ma9WHUPx8p8luz7EBdsjm1MQYRrtC3NiH2TJaQqJTQenevZ8_YoKUUk6VddVzAxc0xycnmY-g94zEVJyjdKeOM98AjEE9C4oX5XgGuKA"
Enter fullscreen mode Exit fullscreen mode

output

home
Enter fullscreen mode Exit fullscreen mode

Source code

https://github.com/namantam1/spring-tutorial/tree/jwt-oauth

Conclusion

This code provides a comprehensive example of setting up JWT-based authentication in a Spring Boot application. It includes the creation of users, secure password storage, and JWT token generation. This setup ensures that only authenticated users can access certain endpoints, enhancing the security of your application.

Top comments (0)