JWT Authentication in Spring Boot with Kotlin: From Zero to Production

December 30, 2024 5 min read Intermediate

Introduction

JWT (JSON Web Token) authentication is a stateless authentication mechanism widely used in modern web applications. This guide will walk you through implementing a production-ready JWT authentication system in a Spring Boot application using Kotlin.

By following this tutorial, you will:

  • Understand how JWT authentication works
  • Set up a Spring Boot project with Kotlin and necessary dependencies
  • Implement user registration and login
  • Create and validate JWT tokens
  • Secure your endpoints with JWT authentication
  • Add refresh token functionality
  • Implement best practices for production deployment

Prerequisites

To follow this tutorial, you will need:

  • JDK 17 or later installed
  • An IDE (IntelliJ IDEA recommended)
  • Basic knowledge of Kotlin and Spring Boot
  • Gradle or Maven for dependency management
  • (Optional) Postman or similar tool for API testing

Step 1: Project Setup

First, let's create a new Spring Boot project. You can use Spring Initializr (https://start.spring.io) with the following dependencies:

  • Spring Boot Starter Web
  • Spring Boot Starter Security
  • Spring Boot Starter Data JPA
  • PostgreSQL Driver
  • jjwt-api (for JWT handling)

Here's the complete build.gradle.kts configuration:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "3.2.1"
    id("io.spring.dependency-management") version "1.1.4"
    kotlin("jvm") version "1.9.21"
    kotlin("plugin.spring") version "1.9.21"
    kotlin("plugin.jpa") version "1.9.21"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    
    // JWT Dependencies
    implementation("io.jsonwebtoken:jjwt-api:0.12.3")
    runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3")
    runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3")
    
    // Database
    runtimeOnly("org.postgresql:postgresql")
    
    // Testing
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.springframework.security:spring-security-test")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs += "-Xjsr305=strict"
        jvmTarget = "17"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

Step 2: Setting Up the Database Models

Let's create our user model that will store authentication information. Create a new file User.kt:

package com.example.auth.model

import jakarta.persistence.*
import java.time.Instant

@Entity
@Table(name = "users")
data class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    
    @Column(unique = true)
    val email: String,
    
    val password: String,
    
    val firstName: String,
    
    val lastName: String,
    
    @Column(nullable = false)
    val createdAt: Instant = Instant.now(),
    
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "user_roles", joinColumns = [JoinColumn(name = "user_id")])
    @Column(name = "role")
    val roles: Set<String> = setOf("ROLE_USER")
)

data class RefreshToken(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    
    @OneToOne
    @JoinColumn(name = "user_id", referencedColumnName = "id")
    val user: User,
    
    @Column(nullable = false, unique = true)
    val token: String,
    
    @Column(nullable = false)
    val expiryDate: Instant
)

Step 3: Implementing the JWT Service

Create a service to handle JWT token generation and validation.

package com.example.auth.service

import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.security.Keys
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Service
import java.security.Key
import java.util.*

@Service
class JwtService {
    @Value("\${jwt.secret}")
    private lateinit var secret: String
    
    @Value("\${jwt.expiration}")
    private lateinit var expirationTime: String
    
    private val key: Key by lazy {
        Keys.hmacShaKeyFor(secret.toByteArray())
    }
    
    fun generateToken(userDetails: UserDetails): String {
        return Jwts.builder()
            .setSubject(userDetails.username)
            .claim("roles", userDetails.authorities.map { it.authority })
            .setIssuedAt(Date())
            .setExpiration(Date(System.currentTimeMillis() + expirationTime.toLong()))
            .signWith(key)
            .compact()
    }
    
    fun validateToken(token: String, userDetails: UserDetails): Boolean {
        val claims = extractAllClaims(token)
        val username = extractUsername(claims)
        return (username == userDetails.username && !isTokenExpired(claims))
    }
    
    private fun extractUsername(claims: Claims): String {
        return claims.subject
    }
    
    private fun isTokenExpired(claims: Claims): Boolean {
        return claims.expiration.before(Date())
    }
    
    private fun extractAllClaims(token: String): Claims {
        return Jwts.parserBuilder()
            .setSigningKey(key)
            .build()
            .parseClaimsJws(token)
            .body
    }
}

Step 4: Security Configuration

Configure Spring Security to use JWT authentication:

package com.example.auth.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

@Configuration
@EnableWebSecurity
class SecurityConfig(
    private val jwtAuthenticationFilter: JwtAuthenticationFilter
) {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .csrf { it.disable() }
            .authorizeHttpRequests { auth ->
                auth
                    .requestMatchers("/api/auth/**").permitAll()
                    .anyRequest().authenticated()
            }
            .sessionManagement { session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            }
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
        
        return http.build()
    }
}

Step 5: Creating the Authentication Controller

Implement the authentication endpoints:

package com.example.auth.controller

import com.example.auth.dto.LoginRequest
import com.example.auth.dto.RegisterRequest
import com.example.auth.service.AuthService
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/auth")
class AuthController(
    private val authService: AuthService
) {

    @PostMapping("/register")
    fun register(@RequestBody request: RegisterRequest): ResponseEntity<AuthResponse> {
        return ResponseEntity.ok(authService.register(request))
    }

    @PostMapping("/login")
    fun login(@RequestBody request: LoginRequest): ResponseEntity<AuthResponse> {
        return ResponseEntity.ok(authService.login(request))
    }

    @PostMapping("/refresh")
    fun refreshToken(@RequestBody request: RefreshTokenRequest): ResponseEntity<AuthResponse> {
        return ResponseEntity.ok(authService.refreshToken(request))
    }
}

Step 6: Production Considerations

When deploying to production, consider implementing these security best practices:

  1. Secure JWT Secret Management:
# application.yml
jwt:
  secret: ${JWT_SECRET:your-256-bit-secret}
  expiration: ${JWT_EXPIRATION:86400000} # 24 hours in milliseconds
  1. Rate Limiting:
    Add rate limiting to prevent brute force attacks:
@Configuration
class RateLimitConfig {
    @Bean
    fun customKeyGenerator(): KeyGenerator {
        return KeyGenerator { request ->
            val ip = request.remoteAddr
            val path = request.requestURI
            "$ip:$path"
        }
    }
}
  1. Request Logging:
    Implement request logging for security monitoring:
@Component
class RequestLoggingFilter : OncePerRequestFilter() {
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val startTime = System.currentTimeMillis()
        
        filterChain.doFilter(request, response)
        
        val duration = System.currentTimeMillis() - startTime
        logger.info(
            "Request completed: method=${request.method}, uri=${request.requestURI}, " +
            "status=${response.status}, duration=${duration}ms"
        )
    }
}

Testing the Implementation

Here's a test class to verify our JWT implementation:

@SpringBootTest
class JwtServiceTest {
    @Autowired
    private lateinit var jwtService: JwtService
    
    @Test
    fun `should generate valid JWT token`() {
        val userDetails = User(
            email = "[email protected]",
            password = "password",
            roles = setOf("ROLE_USER")
        )
        
        val token = jwtService.generateToken(userDetails)
        
        assertThat(jwtService.validateToken(token, userDetails)).isTrue
    }
}

Conclusion

You now have a production-ready JWT authentication system implemented in your Spring Boot Kotlin application. This implementation includes:

  • Secure user registration and login
  • JWT token generation and validation
  • Refresh token functionality
  • Rate limiting
  • Security logging
  • Best practices for production deployment

Remember to:

  • Regularly rotate JWT secrets
  • Monitor failed authentication attempts
  • Keep dependencies updated
  • Implement proper error handling
  • Use HTTPS in production
  • Set appropriate token expiration times

Additional Resources

Latest Articles

Role-Based Access Control (RBAC) in Spring Security with Kotlin

Role-Based Access Control (RBAC) in Spring Security with Kotlin

4 min read Security · rest

Master Role-Based Access Control (RBAC) in Spring Boot applications using Kotlin with practical examples, from basic setup to advanced configurations with method-level security

Implementing API Key Authentication in Spring Boot with Kotlin: A Complete Guide (2024)

Implementing API Key Authentication in Spring Boot with Kotlin: A Complete Guide (2024)

4 min read Security · rest

Learn how to implement secure API key authentication in Spring Boot with Kotlin, including rate limiting, monitoring, and production-ready best practices - complete with practical examples and ready-to-use code

Why You Should Disable CSRF Protection for REST APIs in Spring Boot (And How to Do It Right)

Why You Should Disable CSRF Protection for REST APIs in Spring Boot (And How to Do It Right)

2 min read Security · rest

Learn why CSRF protection isn't necessary for most REST APIs and how to properly disable it in Spring Boot while maintaining security. Includes code examples and best practices