Spring Security Kotlin
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
- Spring Security Documentation - Official documentation
- Spring Security Kotlin DSL Reference - Kotlin DSL reference
- Spring Security with Kotlin DSL | Baeldung - Kotlin DSL guide
- Spring Security JWT With Kotlin | Baeldung - JWT implementation guide
- Spring Security Samples - Sample code collection
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()
}
}