Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ class AuthService(

user.lastLoginAt = LocalDateTime.now()
userRepository.save(user)
return jwtTokenProvider.generateJwtToken(user.id!!, user)
val jwtToken = jwtTokenProvider.generateJwtToken(user.id!!, user)
refreshTokenRepository.save(RefreshToken(user.id!!, jwtToken.refreshToken!!))

return jwtToken
}

@Transactional
Expand All @@ -50,16 +53,11 @@ class AuthService(
}

@Transactional
fun reissue(jwtToken: JwtToken): JwtToken? {
if (!jwtTokenProvider.validateToken(jwtToken.refreshToken!!)) {
throw BusinessException("Refresh Token is Not Valid")
}

val userId: Long = jwtTokenProvider.getRefreshTokenSubject(jwtToken.refreshToken)
fun reissue(jwtToken: String, userId: Long): JwtToken? {
val refreshToken: RefreshToken = refreshTokenRepository.findById(userId)
.orElseThrow { BusinessException("Log-out user") }

if (refreshToken.value != jwtToken.refreshToken) {
if (refreshToken.value != jwtToken) {
throw BusinessException("Refresh Token is Not Valid")
}

Expand All @@ -70,11 +68,16 @@ class AuthService(
jwtTokenProvider.isRefreshTokenRenewalRequired(refreshToken.value) -> {
jwtTokenProvider.generateJwtToken(userId, user).also {
refreshToken.value = it.refreshToken.toString()
refreshTokenRepository.save(refreshToken)
}
}

else -> jwtTokenProvider.generateJwtAccessToken(userId, user, Date())
else -> {
jwtTokenProvider.generateJwtAccessToken(userId, user, Date(), refreshToken.value)
}
}
}

@Transactional
fun logout(userId: Long) {
refreshTokenRepository.deleteById(userId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.lightswitch.controller

import com.lightswitch.application.service.AuthService
import com.lightswitch.controller.request.UserAccount
import com.lightswitch.infrastructure.security.JwtToken
import com.lightswitch.presentation.model.PayloadResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import jakarta.validation.Valid
import jakarta.validation.constraints.NotEmpty
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/v1/users")
class UserController(private val authService: AuthService) {
@Operation(
summary = "Login the user",
description = "Authenticates a user with their username and password, and returns a JWT token."
)
@PostMapping("/login")
fun userLogin(@RequestBody @Valid body: UserAccount): PayloadResponse<JwtToken> {
val token = authService.login(body.username, body.password)
return PayloadResponse(
"Success",
message = "User Login Success",
data = token
)
}

@Operation(
summary = "Initialize user account",
description = "Registers a new user with the provided username and password. Returns a confirmation message."
)
@PostMapping("/initialize")
fun userInitialize(@RequestBody @Valid body: UserAccount): PayloadResponse<String> {
val user = authService.signup(body.username, body.password)
return PayloadResponse(
"Success",
message = "Signup New User Success",
data = user.username
)
}

@Operation(
summary = "Refresh authentication token",
description = "Reissues a new JWT token using the provided refresh token and current user's identity."
)
@PutMapping("/auth/refresh")
fun refreshUserToken(@RequestHeader("Authorization") @NotEmpty @Parameter(description = "The refresh token prefixed with 'Bearer '.") refreshToken: String): PayloadResponse<JwtToken> {
val authentication: Authentication = SecurityContextHolder.getContext().authentication
val token = authService.reissue(refreshToken.removePrefix("Bearer "), authentication.name.toLong())
return PayloadResponse("Success", "Refresh Token Success", token)
}

@Operation(
summary = "Logout the user",
description = "Logs out the user by invalidating their access token. Requires the user to provide their token."
)
@PostMapping("/logout")
fun userLogout(
@RequestHeader("Authorization") @NotEmpty @Parameter(description = "The access token prefixed with 'Bearer '.") accessToken: String,
@RequestBody @Valid body: UserAccount,
): PayloadResponse<String> {
val authentication: Authentication = SecurityContextHolder.getContext().authentication
val userId = authentication.name
authService.logout(userId.toLong())
return PayloadResponse("Success", "Logout Success", body.username)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.lightswitch.controller.request

import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank

@Schema(description = "Represents a user's account credentials including username and password.")
data class UserAccount(
@Schema(description = "The username of the user.")
@field:NotBlank(message = "Username is required.")
val username: String,
@Schema(description = "The password of the user.")
@field:NotBlank(message = "Password is required.")
val password: String
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package com.lightswitch.infrastructure.security

import io.swagger.v3.oas.annotations.media.Schema

@Schema(description = "Represents the JWT token response containing both access and refresh tokens, as well as the expiration date of the access token.")
data class JwtToken(
@Schema(description = "The access token used to authenticate the user in subsequent requests. This token is valid for 30 minutes.")
val accessToken: String? = null,
@Schema(description = "The refresh token used to obtain a new access token once it expires. This token is valid for 7 days.")
val refreshToken: String? = null,
@Schema(description = "The expiration date (timestamp in milliseconds) of the access token. The token becomes invalid after this time.")
val accessTokenExpiredDate: Long? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import org.springframework.web.filter.OncePerRequestFilter
import java.io.IOException

@Component
class JwtTokenFilter(private val jwtTokenProvider: JwtTokenProvider) : OncePerRequestFilter() {
class JwtTokenFilter(
private val jwtTokenProvider: JwtTokenProvider,
) : OncePerRequestFilter() {

private val HEADER_STRING = "Authorization"
private val TOKEN_PREFIX = "Bearer "
companion object {
const val HEADER_STRING = "Authorization"
const val TOKEN_PREFIX = "Bearer "
}

@Throws(ServletException::class, IOException::class)
public override fun doFilterInternal(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ import javax.crypto.SecretKey
class JwtTokenProvider(@Value("\${jwt.secret}") private var secretKey: String) {

private val logger: Logger = LoggerFactory.getLogger(JwtTokenProvider::class.java)
private val TYPE = "tokenType"
private val USER = "user"
private val accessValidTime = 30 * 60 * 1000L // 30 minutes
private val refreshValidTime = 7 * 24 * 60 * 60 * 1000L // 7 days
private val threeDays = 3 * 24 * 60 * 60 * 1000L // 3 days

fun generateJwtToken(userId: Long, user: User): JwtToken {
companion object {
const val TYPE = "tokenType"
const val USER = "user"
const val ACCESS_VALID_TIME = 30 * 60 * 1000L // 30 minutes
const val REFRESH_VALID_TIME = 7 * 24 * 60 * 60 * 1000L // 7 days
const val THREE_DAYS = 3 * 24 * 60 * 60 * 1000L // 3 days
}

fun generateJwtToken(userId: Long, user: User): JwtToken {
val now = Date()
val accessToken = createAccessToken(userId, user, now)

Expand All @@ -35,14 +37,14 @@ class JwtTokenProvider(@Value("\${jwt.secret}") private var secretKey: String) {
val refreshToken = Jwts.builder()
.setClaims(refreshTokenClaims)
.setIssuedAt(now)
.setExpiration(Date(now.time + refreshValidTime))
.setExpiration(Date(now.time + REFRESH_VALID_TIME))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact()

return JwtToken(
accessToken = accessToken,
refreshToken = refreshToken,
accessTokenExpiredDate = accessValidTime
accessTokenExpiredDate = ACCESS_VALID_TIME
)
}

Expand All @@ -59,7 +61,7 @@ class JwtTokenProvider(@Value("\${jwt.secret}") private var secretKey: String) {
val accessToken = Jwts.builder()
.setClaims(accessTokenClaims)
.setIssuedAt(now)
.setExpiration(Date(now.time + accessValidTime))
.setExpiration(Date(now.time + ACCESS_VALID_TIME))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact()
return accessToken
Expand All @@ -81,12 +83,12 @@ class JwtTokenProvider(@Value("\${jwt.secret}") private var secretKey: String) {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey))
}

fun generateJwtAccessToken(userId: Long, user: User, now: Date): JwtToken {
fun generateJwtAccessToken(userId: Long, user: User, now: Date, refreshToken: String): JwtToken {
val accessToken = createAccessToken(userId, user, now)
return JwtToken(
accessToken = accessToken,
refreshToken = null,
accessTokenExpiredDate = accessValidTime
refreshToken = refreshToken,
accessTokenExpiredDate = ACCESS_VALID_TIME
)
}

Expand Down Expand Up @@ -149,7 +151,7 @@ class JwtTokenProvider(@Value("\${jwt.secret}") private var secretKey: String) {
val now = (Date()).time
val refreshExpiredTime = claimsJws.body.expiration.time

return refreshExpiredTime - now > threeDays
return refreshExpiredTime - now <= THREE_DAYS
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ class SecurityConfig(
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {

http
.csrf {
it.disable()
}
.authorizeHttpRequests { auth ->
auth
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.REQUEST)
.permitAll()
.requestMatchers("/login/**").permitAll()
.requestMatchers("/api/v1/users/**").permitAll()
.anyRequest().authenticated()

}
Expand All @@ -46,7 +49,10 @@ class SecurityConfig(
.formLogin { form ->
form.disable()
}
.addFilterBefore(JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter::class.java)
.addFilterBefore(
JwtTokenFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter::class.java
)

return http.build()
}
Expand Down
Loading
Loading