Skip to content

feat(auth): add base spring security #94

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions kotlin-web-spring-boot-3/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.2.RELEASE")
implementation("io.jsonwebtoken:jjwt-api:0.12.5")
implementation("io.jsonwebtoken:jjwt-impl:0.12.5")
implementation("io.jsonwebtoken:jjwt-jackson:0.12.5")
developmentOnly("org.springframework.boot:spring-boot-devtools:3.2.4")
runtimeOnly("org.postgresql:postgresql:42.7.3")
testImplementation("org.springframework.boot:spring-boot-starter-test:3.2.4")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.vndevteam.kotlinwebspringboot3.application.auth

data class AuthReqDto(
val email: String,
val password: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.vndevteam.kotlinwebspringboot3.application.auth

data class AuthResDto(
val jwtToken: String,
val refreshToken: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.vndevteam.kotlinwebspringboot3.application.auth

import com.vndevteam.kotlinwebspringboot3.domain.auth.AuthService
import org.springframework.http.HttpStatus
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
import org.springframework.web.server.ResponseStatusException

@RestController
@RequestMapping("/auth")
class AuthController(
private val authService: AuthService,
) {
@PostMapping("/login")
fun authenticate(@RequestBody authenticationReqDto: AuthReqDto): AuthResDto =
authService.authentication(authenticationReqDto)

@PostMapping("/refresh")
fun refreshAccessToken(@RequestBody request: RefreshTokenReqDto): TokenResDto =
authService.refreshAccessToken(request.jwtToken)?.toTokenResDto()
?: throw ResponseStatusException(HttpStatus.FORBIDDEN, "Invalid refresh token.")

private fun String.toTokenResDto(): TokenResDto = TokenResDto(jwtToken = this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.vndevteam.kotlinwebspringboot3.application.auth

data class RefreshTokenReqDto(
val jwtToken: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.vndevteam.kotlinwebspringboot3.application.auth

data class TokenResDto(
val jwtToken: String,
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.vndevteam.kotlinwebspringboot3.application.demo

import com.vndevteam.kotlinwebspringboot3.domain.enums.MESSAGE
import com.vndevteam.kotlinwebspringboot3.infrastructure.util.MsgUtils
import com.vndevteam.kotlinwebspringboot3.common.enums.Message
import com.vndevteam.kotlinwebspringboot3.util.MsgUtils
import jakarta.validation.Valid
import jakarta.validation.constraints.NotBlank
import org.springframework.context.i18n.LocaleContextHolder
Expand All @@ -17,7 +17,7 @@ class DemoRestController {
fun getLocaleMessage(): Map<String, Any> {
return mapOf(
"request_locale" to LocaleContextHolder.getLocale(),
"msg" to MsgUtils.getMessage(MESSAGE.MSG_1)
"msg" to MsgUtils.getMessage(Message.MSG_1)
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.vndevteam.kotlinwebspringboot3.application.post

import java.util.UUID

data class PostEntity(
val id: UUID,
val title: String,
val content: String,
)
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
package com.vndevteam.kotlinwebspringboot3.application.post

class PostResDto {}
import java.util.UUID

data class PostResDto(
val id: UUID,
val title: String,
val content: String,
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package com.vndevteam.kotlinwebspringboot3.application.post

import com.vndevteam.kotlinwebspringboot3.domain.post.PostService
import com.vndevteam.kotlinwebspringboot3.util.toPostResDto
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController @RequestMapping("/posts") class PostRestController {}
@RestController
@RequestMapping("/posts")
class PostRestController(private val postService: PostService) {
@GetMapping fun listAll(): List<PostResDto> = postService.findAll().map { it.toPostResDto() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.vndevteam.kotlinwebspringboot3.application.user

import com.vndevteam.kotlinwebspringboot3.common.enums.UserRole
import java.util.UUID

data class UserEntity(val id: UUID, val email: String, val password: String, val role: UserRole)
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
package com.vndevteam.kotlinwebspringboot3.application.user

class UserReqDto {}
data class UserReqDto(
val email: String,
val password: String,
)
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
package com.vndevteam.kotlinwebspringboot3.application.user

class UserResDto {}
import java.util.UUID

data class UserResDto(
val uuid: UUID,
val email: String,
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,40 @@
package com.vndevteam.kotlinwebspringboot3.application.user

import com.vndevteam.kotlinwebspringboot3.domain.user.UserService
import com.vndevteam.kotlinwebspringboot3.util.toUserEntity
import com.vndevteam.kotlinwebspringboot3.util.toUserResDto
import java.util.UUID
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
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
import org.springframework.web.server.ResponseStatusException

@RestController @RequestMapping("/users") class UserRestController {}
@RestController
@RequestMapping("/users")
class UserRestController(private val userService: UserService) {
@PostMapping
fun create(@RequestBody userReqDto: UserReqDto): UserResDto =
userService.createUser(userReqDto.toUserEntity())?.toUserResDto()
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot create user.")

@GetMapping fun listAll(): List<UserResDto> = userService.findAll().map { it.toUserResDto() }

@GetMapping("/{uuid}")
fun findByUUID(@PathVariable uuid: UUID): UserResDto =
userService.findByUUID(uuid)?.toUserResDto()
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found.")

@DeleteMapping("/{uuid}")
fun deleteByUUID(@PathVariable uuid: UUID): ResponseEntity<Boolean> {
val success = userService.deleteByUUID(uuid)

return if (success) ResponseEntity.noContent().build()
else throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found.")
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.vndevteam.kotlinwebspringboot3.infrastructure.exception
package com.vndevteam.kotlinwebspringboot3.common.constants

class ErrorConstants {
companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.vndevteam.kotlinwebspringboot3.common.constants

class SecurityConstants {
companion object {
const val BEARER = "Bearer "
const val HEADER_ACCESS_TOKEN = "access-token"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.vndevteam.kotlinwebspringboot3.common.enums

enum class Message(val code: String) {
MSG_1("msg-1"),
MSG_2("msg-2"),
ERR_1("err-1")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.vndevteam.kotlinwebspringboot3.common.enums

enum class UserRole {
USER,
ADMIN
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.vndevteam.kotlinwebspringboot3.infrastructure.exception.error
package com.vndevteam.kotlinwebspringboot3.common.error

import com.fasterxml.jackson.annotation.JsonInclude

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.vndevteam.kotlinwebspringboot3.infrastructure.exception.error
package com.vndevteam.kotlinwebspringboot3.common.error

import com.fasterxml.jackson.annotation.JsonFormat
import java.time.LocalDateTime
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.vndevteam.kotlinwebspringboot3.domain.auth

import com.vndevteam.kotlinwebspringboot3.application.auth.AuthReqDto
import com.vndevteam.kotlinwebspringboot3.application.auth.AuthResDto
import com.vndevteam.kotlinwebspringboot3.infrastructure.config.JwtProperties
import com.vndevteam.kotlinwebspringboot3.util.DateTimeUtils
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Service

@Service
class AuthService(
private val authManager: AuthenticationManager,
private val userDetailsService: CustomUserDetailsService,
private val tokenService: TokenService,
private val jwtProperties: JwtProperties,
private val refreshTokenRepository: RefreshTokenRepository,
) {
fun authentication(authenticationRequest: AuthReqDto): AuthResDto {
authManager.authenticate(
UsernamePasswordAuthenticationToken(
authenticationRequest.email,
authenticationRequest.password,
)
)

val user = userDetailsService.loadUserByUsername(authenticationRequest.email)

return AuthResDto(
jwtToken = createJwtToken(user),
refreshToken = createRefreshToken(user),
)
}

fun refreshAccessToken(refreshToken: String): String? {
val extractedEmail = tokenService.extractEmail(refreshToken)
return extractedEmail?.let { email ->
val currentUserDetails = userDetailsService.loadUserByUsername(email)
val refreshTokenUserDetails =
refreshTokenRepository.findUserDetailsByToken(refreshToken)
if (
!tokenService.isExpired(refreshToken) &&
refreshTokenUserDetails?.username == currentUserDetails.username
)
createJwtToken(currentUserDetails)
else null
}
}

private fun createJwtToken(user: UserDetails) =
tokenService.generate(
userDetails = user,
expirationDate =
DateTimeUtils.getDateNow(
DateTimeUtils.getNow().plusMinutes(jwtProperties.jwtTokenExpiration)
)
)

private fun createRefreshToken(user: UserDetails) =
tokenService.generate(
userDetails = user,
expirationDate =
DateTimeUtils.getDateNow(
DateTimeUtils.getNow().plusMinutes(jwtProperties.refreshTokenExpiration)
)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.vndevteam.kotlinwebspringboot3.domain.auth

import com.vndevteam.kotlinwebspringboot3.domain.user.UserRepository
import com.vndevteam.kotlinwebspringboot3.util.toUserDetails
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Service

@Service
class CustomUserDetailsService(private val userRepository: UserRepository) : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails =
userRepository.findByEmail(username)?.toUserDetails()
?: throw UsernameNotFoundException("Not found!")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.vndevteam.kotlinwebspringboot3.domain.auth

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

@Component
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,46 @@
package com.vndevteam.kotlinwebspringboot3.domain.auth

import com.vndevteam.kotlinwebspringboot3.infrastructure.config.JwtProperties
import com.vndevteam.kotlinwebspringboot3.util.DateTimeUtils
import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys
import java.util.Date
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Service

@Service
class TokenService(jwtProperties: JwtProperties) {
private val secretKey = Keys.hmacShaKeyFor(jwtProperties.key.toByteArray())

fun generate(
userDetails: UserDetails,
expirationDate: Date,
additionalClaims: Map<String, Any> = emptyMap()
): String =
Jwts.builder()
.claims()
.subject(userDetails.username)
.issuedAt(DateTimeUtils.getDateNow())
.expiration(expirationDate)
.add(additionalClaims)
.and()
.signWith(secretKey)
.compact()

fun isValid(token: String, userDetails: UserDetails): Boolean {
val email = extractEmail(token)
return userDetails.username == email && !isExpired(token)
}

fun extractEmail(token: String): String? = getAllClaims(token).subject

fun isExpired(token: String): Boolean =
getAllClaims(token).expiration.before(DateTimeUtils.getDateNow())

private fun getAllClaims(token: String): Claims {
val parser = Jwts.parser().verifyWith(secretKey).build()

return parser.parseSignedClaims(token).payload
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.vndevteam.kotlinwebspringboot3.domain.post

import com.vndevteam.kotlinwebspringboot3.application.post.PostEntity
import java.util.UUID
import org.springframework.stereotype.Repository

@Repository
class PostRepository {
private val posts =
listOf(
PostEntity(id = UUID.randomUUID(), title = "Post 1", content = "Content 1"),
PostEntity(id = UUID.randomUUID(), title = "Post 2", content = "Content 2"),
)

fun findAll(): List<PostEntity> = posts
}
Loading