Skip to content

Commit

Permalink
[KTL-767] Spring Security JWT with Kotlin (Baeldung#1116)
Browse files Browse the repository at this point in the history
* feat: kotlin spring boot jwt

* fix: pom.xml

* fix: indent

* fix: remove * import
  • Loading branch information
lucaCambi77 authored Oct 10, 2024
1 parent df2da47 commit f012ae4
Show file tree
Hide file tree
Showing 19 changed files with 720 additions and 0 deletions.
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@
<module>spring-mvc-kotlin</module>
<module>spring-reactive-kotlin</module>
<module>spring-security-kotlin-dsl</module>
<module>spring-security-kotlin</module>
</modules>
</profile>

Expand Down
1 change: 1 addition & 0 deletions spring-security-kotlin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
### Relevant Articles:
114 changes: 114 additions & 0 deletions spring-security-kotlin/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-kotlin</artifactId>
<version>1.0</version>
<packaging>jar</packaging>

<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-3</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../parent-boot-3</relativePath>
</parent>

<properties>
<jwt.version>0.11.5</jwt.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>

<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>

<!-- JJWT API -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>

<!-- JJWT Implementation -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>

<!-- JJWT Jackson Serializer -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>

<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
</dependencies>

<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.baeldung.security.jwt

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity

@EnableWebSecurity
@SpringBootApplication
class JwtApplication

fun main(args: Array<String>) {
runApplication<JwtApplication>(*args)
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.baeldung.security.jwt.controller

import com.baeldung.security.jwt.domain.AuthenticationRequest
import com.baeldung.security.jwt.domain.AuthenticationResponse
import com.baeldung.security.jwt.domain.RefreshTokenRequest
import com.baeldung.security.jwt.domain.TokenResponse
import com.baeldung.security.jwt.service.AuthenticationService
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/auth")
class AuthController(
private val authenticationService: AuthenticationService
) {
@PostMapping
fun authenticate(
@RequestBody authRequest: AuthenticationRequest
): AuthenticationResponse =
authenticationService.authentication(authRequest)

@PostMapping("/refresh")
fun refreshAccessToken(
@RequestBody request: RefreshTokenRequest
): TokenResponse = TokenResponse(token = authenticationService.refreshAccessToken(request.token))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.baeldung.security.jwt.controller

import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api")
class HelloController {
@GetMapping("/hello")
fun hello(): ResponseEntity<String> {
return ResponseEntity.ok("Hello, Authorized User!")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.baeldung.security.jwt.controller

import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.security.SignatureException
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.core.AuthenticationException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler

@ControllerAdvice
class JwtControllerAdvice {
@ExceptionHandler(value = [ExpiredJwtException::class, AuthenticationException::class, SignatureException::class])
fun handleAuthenticationExceptions(ex: RuntimeException): ResponseEntity<String> {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("Authentication failed: ${ex.message}")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.baeldung.security.jwt.domain

data class AuthenticationRequest(
val username: String,
val password: String,
)

data class AuthenticationResponse(
val accessToken: String,
val refreshToken: String,
)

data class RefreshTokenRequest(
val token: String
)

data class TokenResponse(
val token: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.baeldung.security.jwt.domain

import java.util.UUID

data class User(
val id: UUID,
val name: String,
val password: String,
val role: Role
)

enum class Role {
USER, ADMIN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.baeldung.security.jwt.filter

import com.baeldung.security.jwt.service.TokenService
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter

@Component
class JwtAuthorizationFilter(
private val userDetailsService: UserDetailsService,
private val tokenService: TokenService
) : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val authorizationHeader: String? = request.getHeader("Authorization")

if (null != authorizationHeader && authorizationHeader.startsWith("Bearer ")) {
try {
val token: String = authorizationHeader.substringAfter("Bearer ")
val username: String = tokenService.extractUsername(token)

if (SecurityContextHolder.getContext().authentication == null) {
val userDetails: UserDetails = userDetailsService.loadUserByUsername(username)

if (username == userDetails.username) {
val authToken = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
authToken.details = WebAuthenticationDetailsSource().buildDetails(request)
SecurityContextHolder.getContext().authentication = authToken
}
}
} catch (ex: Exception) {
response.writer.write("""{"error": "Filter Authorization error: ${ex.message ?: "unknown error"}"}""")
}
}

filterChain.doFilter(request, response)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.baeldung.security.jwt.repository

import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Repository

@Repository
class RefreshTokenRepository {
private val tokens = mutableMapOf<String, UserDetails>()

fun findUserDetailsByToken(token: String): UserDetails? =
tokens[token]

fun save(token: String, userDetails: UserDetails) {
tokens[token] = userDetails
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.baeldung.security.jwt.repository

import com.baeldung.security.jwt.domain.Role
import com.baeldung.security.jwt.domain.User
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Repository
import java.util.UUID

@Repository
class UserRepository(
encoder: PasswordEncoder
) {
private val users = mutableSetOf(
User(
id = UUID.randomUUID(),
name = "[email protected]",
password = encoder.encode("pass1"),
role = Role.USER,
),
User(
id = UUID.randomUUID(),
name = "[email protected]",
password = encoder.encode("pass2"),
role = Role.ADMIN,
),
User(
id = UUID.randomUUID(),
name = "[email protected]",
password = encoder.encode("pass3"),
role = Role.USER,
)
)

fun findByUsername(email: String): User? =
users
.firstOrNull { it.name == email }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.baeldung.security.jwt.security

import com.baeldung.security.jwt.repository.UserRepository
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException

class JwtUserDetailsService(
private val userRepository: UserRepository
) : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
val user = userRepository.findByUsername(username)
?: throw UsernameNotFoundException("User $username not found!")

return User.builder()
.username(user.name)
.password(user.password)
.roles(user.role.name)
.build()
}
}
Loading

0 comments on commit f012ae4

Please sign in to comment.