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
- Spring web (For setting up servlet)
- Spring data jdbc and h2 (For database and ORM)
- Spring oauth2 authorization server (For JWT authorisation)
- 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>
Next we will create a protected REST endpoint to test our API.
class HomeController {
@GetMapping
String home() {
return "home";
}
}
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;
}
}
Creating user repository
import org.springframework.data.repository.CrudRepository;
interface UserRepository extends CrudRepository<User, Long> {
Optional<User> findByUsername(String username);
}
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
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
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();
}
}
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();
}
}
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;
}
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);
};
}
Now let's test our API
> curl -X POST http://localhost:8080/auth/login -d '{"username":"admin", "password": "admin"}' -H "Content-type: application/json"
output
eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzZWxmIiwic3ViIjoiYWRtaW4iLCJleHAiOjE3MjIwMzUyOTksImlhdCI6MTcyMjAzMTY5OSwic2NvcGUiOiIifQ.SwRW7_P4xMp5-aLTcUYZyL9jncTIVHUFswaQoKupwfhaoAWYRpmP5g-Ras1gU2oAJ_saY7jt0N-WiezS977FAgDOYYmdxr2H4SBeg0qNqsSV6fOX9zRELYukdbEnT9GGaAk5u9WXd6JoHa5x_7vqXJ1Uu2ciDNxgRaBVtwoeWawZ53g0EFwjqturuQALWfBkkLafMbkY_dSqAoohmvTlVVGWiYPylGOsIhQ3CjamZVzqq8ma9WHUPx8p8luz7EBdsjm1MQYRrtC3NiH2TJaQqJTQenevZ8_YoKUUk6VddVzAxc0xycnmY-g94zEVJyjdKeOM98AjEE9C4oX5XgGuKA
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"
output
home
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)