Spring Security Kotlin

Authentication LibrarySpring SecurityKotlinJWTOAuth2DSLAuthenticationAuthorizationSecurity

Authentication Library

Spring Security Kotlin

Overview

Spring Security Kotlin provides Domain Specific Language (DSL) support for using Spring Framework's security framework Spring Security with the Kotlin language. It enables comprehensive security features including JWT token authentication, OAuth2, SAML, form authentication, and Basic authentication through Kotlin's concise and expressive syntax.

Details

Spring Security Kotlin is officially supported from Spring Security 6 onwards, allowing security configurations to be written using Kotlin DSL. Compared to traditional Java syntax, the combination of Kotlin's functional programming and DSL provides more readable and maintainable security configurations. It offers all the security features required for enterprise-level web applications, including JWT authentication, OAuth2 resource servers, authorization rules, CSRF protection, and session management.

Key Features

  • Kotlin DSL: Intuitive security configuration leveraging Kotlin syntax
  • JWT Support: Complete support for JWT token-based authentication
  • OAuth2 Integration: OAuth2 client, resource server, and authorization server support
  • Multiple Authentication Methods: Form, Basic, Bearer Token, SAML2, and more
  • Fine-grained Authorization Control: Authorization rules at URL, method, and domain levels
  • CSRF Protection: Cross-site request forgery countermeasures
  • Session Management: Session fixation attack prevention and session control

Supported Authentication Methods

  • Form Authentication: Web-based login forms
  • HTTP Basic: Basic authentication headers
  • Bearer Token: JWT, OAuth2 access tokens
  • OAuth2 Login: External providers like Google, GitHub, Facebook
  • SAML2: SAML 2.0 single sign-on
  • X.509 Certificates: Client certificate authentication
  • RememberMe: Persistent login functionality

Pros and Cons

Pros

  • Kotlin DSL: Type-safe and expressive security configuration
  • Comprehensive Security: Complete coverage from authentication to authorization
  • Spring Integration: Full integration with Spring Boot and Spring WebFlux
  • Flexible Configuration: Support for YAML, annotation, and programmatic configuration
  • Rich Documentation: Official documentation and community support
  • Enterprise Ready: Proven track record in large-scale systems
  • Active Development: Continuous development and support by Spring team

Cons

  • Learning Curve: Requires knowledge of both Spring Security and Kotlin
  • Configuration Complexity: Complex configuration when using advanced features
  • Performance: Slight overhead due to multiple features
  • Kotlin Specific: Kotlin-specific DSL (Java can still be used traditionally)
  • Version Dependencies: Need to manage compatibility with Spring Boot versions

Reference Links

Code Examples

Basic Setup

// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.security:spring-security-config")
    implementation("io.jsonwebtoken:jjwt-api:0.11.5")
    implementation("io.jsonwebtoken:jjwt-impl:0.11.5")
    implementation("io.jsonwebtoken:jjwt-jackson:0.11.5")
}

Basic Security Configuration

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.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize("/login", permitAll)
                authorize("/public/**", permitAll)
                authorize(anyRequest, authenticated)
            }
            formLogin {
                loginPage = "/login"
                defaultSuccessUrl = "/dashboard"
                failureUrl = "/login?error=true"
            }
            logout {
                logoutUrl = "/logout"
                logoutSuccessUrl = "/login"
                deleteCookies("JSESSIONID")
            }
            csrf { 
                disable() 
            }
        }
        return http.build()
    }
}

JWT Authentication Configuration

import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
import java.nio.charset.StandardCharsets

@Configuration
@EnableWebSecurity
class JwtSecurityConfig {

    private val jwtSecret = "your-256-bit-secret-key-for-jwt-tokens-here"

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            sessionManagement {
                sessionCreationPolicy = SessionCreationPolicy.STATELESS
            }
            authorizeHttpRequests {
                authorize("/api/auth/**", permitAll)
                authorize("/api/public/**", permitAll)
                authorize("/api/**", authenticated)
                authorize(anyRequest, permitAll)
            }
            oauth2ResourceServer {
                jwt {
                    jwtDecoder = jwtDecoder()
                }
            }
            csrf { 
                disable() 
            }
        }
        return http.build()
    }

    @Bean
    fun jwtDecoder(): JwtDecoder {
        val secretKey: SecretKey = SecretKeySpec(
            jwtSecret.toByteArray(StandardCharsets.UTF_8), 
            "HmacSHA256"
        )
        return NimbusJwtDecoder.withSecretKey(secretKey).build()
    }
}

OAuth2 Login Configuration

@Configuration
@EnableWebSecurity
class OAuth2SecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize("/", permitAll)
                authorize("/login", permitAll)
                authorize("/error", permitAll)
                authorize(anyRequest, authenticated)
            }
            oauth2Login {
                loginPage = "/login"
                defaultSuccessUrl = "/dashboard"
                failureUrl = "/login?error=true"
                userInfoEndpoint {
                    userService = customOAuth2UserService()
                }
            }
            logout {
                logoutSuccessUrl = "/"
                invalidateHttpSession = true
                deleteCookies("JSESSIONID")
            }
        }
        return http.build()
    }

    @Bean
    fun customOAuth2UserService(): OAuth2UserService<OAuth2UserRequest, OAuth2User> {
        return CustomOAuth2UserService()
    }
}

// Custom OAuth2 User Service
class CustomOAuth2UserService : DefaultOAuth2UserService() {
    
    override fun loadUser(userRequest: OAuth2UserRequest): OAuth2User {
        val oAuth2User = super.loadUser(userRequest)
        
        // Save or map user information to database
        processOAuthPostLogin(oAuth2User, userRequest.clientRegistration.registrationId)
        
        return oAuth2User
    }
    
    private fun processOAuthPostLogin(oAuth2User: OAuth2User, provider: String) {
        val email = oAuth2User.getAttribute<String>("email")
        val name = oAuth2User.getAttribute<String>("name")
        
        // User information processing logic
        println("OAuth2 User: $name ($email) from $provider")
    }
}

Method-Level Security

import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.access.prepost.PostAuthorize
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.web.bind.annotation.*

@Configuration
@EnableMethodSecurity(prePostEnabled = true)
class MethodSecurityConfig

@RestController
@RequestMapping("/api/admin")
class AdminController {

    @GetMapping("/users")
    @PreAuthorize("hasRole('ADMIN')")
    fun getAllUsers(): List<User> {
        // Only accessible by administrators
        return userService.findAll()
    }

    @GetMapping("/user/{id}")
    @PreAuthorize("hasRole('ADMIN') or @userService.isOwner(authentication.name, #id)")
    fun getUserById(@PathVariable id: Long): User {
        // Only accessible by administrators or the owner
        return userService.findById(id)
    }

    @PostMapping("/user")
    @PreAuthorize("hasAuthority('USER_CREATE')")
    fun createUser(@RequestBody user: User): User {
        return userService.save(user)
    }

    @DeleteMapping("/user/{id}")
    @PreAuthorize("hasRole('SUPER_ADMIN')")
    fun deleteUser(@PathVariable id: Long) {
        userService.deleteById(id)
    }
}

Multiple Authentication Methods Configuration

@Configuration
@EnableWebSecurity
class MultiAuthSecurityConfig {

    @Bean
    @Order(1)
    fun apiSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            securityMatcher("/api/**")
            sessionManagement {
                sessionCreationPolicy = SessionCreationPolicy.STATELESS
            }
            authorizeHttpRequests {
                authorize("/api/auth/**", permitAll)
                authorize("/api/**", authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwtDecoder = jwtDecoder()
                }
            }
            httpBasic { }
            csrf { disable() }
        }
        return http.build()
    }

    @Bean
    @Order(2)
    fun webSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize("/login", permitAll)
                authorize("/register", permitAll)
                authorize("/public/**", permitAll)
                authorize(anyRequest, authenticated)
            }
            formLogin {
                loginPage = "/login"
                defaultSuccessUrl = "/dashboard"
            }
            oauth2Login {
                loginPage = "/login"
                defaultSuccessUrl = "/dashboard"
            }
            logout {
                logoutUrl = "/logout"
                logoutSuccessUrl = "/login"
            }
        }
        return http.build()
    }
}

Custom Authentication Provider

import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Component

@Component
class CustomAuthenticationProvider(
    private val userService: UserService,
    private val passwordEncoder: PasswordEncoder
) : AuthenticationProvider {

    override fun authenticate(authentication: Authentication): Authentication {
        val username = authentication.name
        val password = authentication.credentials.toString()

        val user = userService.findByUsername(username)
            ?: throw BadCredentialsException("Invalid username or password")

        if (!passwordEncoder.matches(password, user.password)) {
            throw BadCredentialsException("Invalid username or password")
        }

        if (!user.enabled) {
            throw BadCredentialsException("Account is disabled")
        }

        val authorities = user.roles.map { SimpleGrantedAuthority("ROLE_$it") }

        return UsernamePasswordAuthenticationToken(username, password, authorities)
    }

    override fun supports(authentication: Class<*>): Boolean {
        return authentication == UsernamePasswordAuthenticationToken::class.java
    }
}

// Custom Provider Configuration
@Configuration
class AuthConfig(
    private val customAuthenticationProvider: CustomAuthenticationProvider
) {

    @Bean
    fun authenticationManager(): AuthenticationManager {
        return ProviderManager(customAuthenticationProvider)
    }
}

Reactive Security (WebFlux)

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.config.web.server.invoke
import org.springframework.security.web.server.SecurityWebFilterChain

@Configuration
@EnableWebFluxSecurity
class ReactiveSecurityConfig {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        return http {
            authorizeExchange {
                authorize("/api/public/**", permitAll)
                authorize("/api/**", authenticated)
                authorize(anyExchange, permitAll)
            }
            oauth2ResourceServer {
                jwt { }
            }
            csrf { disable() }
        }
    }
}

Security Event Handling

import org.springframework.context.event.EventListener
import org.springframework.security.authentication.event.AuthenticationSuccessEvent
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent
import org.springframework.security.authorization.event.AuthorizationDeniedEvent
import org.springframework.stereotype.Component

@Component
class SecurityEventListener {

    @EventListener
    fun handleAuthenticationSuccess(event: AuthenticationSuccessEvent) {
        val username = event.authentication.name
        println("Successful authentication for user: $username")
        
        // Process on successful login (logging, statistics update, etc.)
        auditService.logSuccessfulLogin(username)
    }

    @EventListener
    fun handleAuthenticationFailure(event: AbstractAuthenticationFailureEvent) {
        val username = event.authentication.name
        val exception = event.exception
        println("Failed authentication for user: $username, reason: ${exception.message}")
        
        // Process on login failure (security logging, account lockout, etc.)
        securityService.handleFailedLogin(username, exception)
    }

    @EventListener
    fun handleAuthorizationDenied(event: AuthorizationDeniedEvent<*>) {
        val username = event.authentication?.name ?: "anonymous"
        println("Access denied for user: $username")
        
        // Process on access denied
        auditService.logAccessDenied(username, event.authorizationDecision.toString())
    }
}

CSRF Protection and Custom Headers

@Configuration
@EnableWebSecurity
class CsrfSecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize("/api/**", authenticated)
                authorize(anyRequest, permitAll)
            }
            csrf {
                csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse()
                ignoringRequestMatchers("/api/webhook/**")
            }
            headers {
                frameOptions {
                    deny()
                }
                contentTypeOptions { }
                httpStrictTransportSecurity {
                    maxAgeInSeconds = 31536000
                    includeSubDomains = true
                }
                referrerPolicy {
                    policy = ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN
                }
            }
        }
        return http.build()
    }
}