Skip to content

Commit

Permalink
changed authentication from UserDetailsService to custom JwtClaims (#54)
Browse files Browse the repository at this point in the history
* changed authentication from UserDetailsService to custom JwtClaims
  • Loading branch information
MocStepan authored Apr 23, 2024
1 parent f85577e commit 1fe233f
Show file tree
Hide file tree
Showing 30 changed files with 448 additions and 778 deletions.
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
package com.tul.backend.auth.base.config

import com.tul.backend.auth.base.service.TokenFilterService
import com.tul.backend.auth.base.service.TokenFilter
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.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter


@Component
class JwtAuthenticationFilter(
private val tokenFilterService: TokenFilterService

private val tokenFilter: TokenFilter
) : OncePerRequestFilter() {

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val userDetails = tokenFilterService.validateRequest(request, response)
if (userDetails != null) {
tokenFilterService.updateContext(userDetails, request, response)
val validClaims = tokenFilter.validateRequest(request)

if (validClaims != null) {
val authToken = UsernamePasswordAuthenticationToken(validClaims, null, listOf(validClaims.authUserRole))
SecurityContextHolder.getContext().authentication = authToken
}

filterChain.doFilter(request, response)
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
package com.tul.backend.auth.base.config

import com.tul.backend.auth.base.service.CustomPasswordEncoder
import com.tul.backend.auth.base.service.CustomUserDetailsService
import com.tul.backend.auth.repository.AuthUserRepository
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler
import org.springframework.security.web.authentication.logout.LogoutHandler
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler
Expand All @@ -20,25 +11,6 @@ import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuc
@Configuration
class JwtConfiguration {

@Bean
fun userDetailService(authUserRepository: AuthUserRepository): UserDetailsService =
CustomUserDetailsService(authUserRepository)

@Bean
fun encoder(): PasswordEncoder = CustomPasswordEncoder()

@Bean
fun authenticationProvider(authUserRepository: AuthUserRepository): AuthenticationProvider =
DaoAuthenticationProvider()
.also {
it.setUserDetailsService(userDetailService(authUserRepository))
it.setPasswordEncoder(encoder())
}

@Bean
fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager =
config.authenticationManager

@Bean
fun logoutSuccessHandler(): LogoutSuccessHandler {
val handler = SimpleUrlLogoutSuccessHandler()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import jakarta.servlet.http.HttpServletResponse
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
Expand All @@ -28,28 +27,25 @@ import org.springframework.web.cors.CorsConfigurationSource
@EnableMethodSecurity
class SecurityConfiguration(
private val objectMapper: ObjectMapper,
private val authenticationProvider: AuthenticationProvider,
private val logoutSuccessHandler: LogoutSuccessHandler,
private val cookieClearingLogoutHandler: LogoutHandler
private val cookieClearingLogoutHandler: LogoutHandler,
private val jwtAuthenticationFilter: JwtAuthenticationFilter
) {

private val userUnsecuredEndpoints =
arrayOf(
"/api/auth/login",
"/api/auth/register",
"/api/weather/current/*",
"/api/weather/forecast/*",
)

private val adminUnsecuredEndpoints =
arrayOf(
"api/auth/test"
"api/auth/test",
)

@Bean
fun securityFilterChain(
http: HttpSecurity,
jwtAuthenticationFilter: JwtAuthenticationFilter
): DefaultSecurityFilterChain =
fun securityFilterChain(http: HttpSecurity): DefaultSecurityFilterChain =
http
.csrf { it.disable() }
.cors {
Expand All @@ -58,13 +54,12 @@ class SecurityConfiguration(
.authorizeHttpRequests {
it
.requestMatchers(*userUnsecuredEndpoints).permitAll()
.requestMatchers(*adminUnsecuredEndpoints).hasRole(AuthUserRole.ADMIN.name)
.anyRequest().fullyAuthenticated()
.requestMatchers(*adminUnsecuredEndpoints).hasAuthority(AuthUserRole.ADMIN.name)
.anyRequest().authenticated()
}
.sessionManagement { session: SessionManagementConfigurer<HttpSecurity> ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
.exceptionHandling {
it.authenticationEntryPoint(authenticationExceptionHandler)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.tul.backend.auth.base.dto

import com.tul.backend.auth.base.valueobject.AuthUserRole
import com.tul.backend.auth.base.valueobject.EmailAddress
import com.tul.backend.auth.entity.AuthUser
import org.springframework.security.core.AuthenticatedPrincipal

data class AccessTokenClaims(
val authUserId: Long,
val authUserRole: AuthUserRole,
val email: EmailAddress
) : AuthenticatedPrincipal {

constructor(authUser: AuthUser) : this(
authUserId = authUser.id,
authUserRole = authUser.role,
email = authUser.email
)

override fun getName(): String = email.value
}
24 changes: 24 additions & 0 deletions backend/src/main/kotlin/com/tul/backend/auth/base/dto/JwtClaims.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.tul.backend.auth.base.dto

import java.util.Date
import kotlin.time.Duration

class JwtClaims(
val claims: Map<String, *>,
val issuedAt: Date,
val expiration: Date
) {

companion object {
fun from(claims: Map<String, *>, duration: Duration): JwtClaims {
val now = Date()
val expiration = Date(now.time + duration.inWholeSeconds)

return JwtClaims(
claims = claims,
issuedAt = now,
expiration = expiration
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.tul.backend.auth.base.service

import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import com.tul.backend.auth.base.dto.AccessTokenClaims
import com.tul.backend.auth.base.dto.JwtClaims
import com.tul.backend.auth.entity.AuthUser
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.ResponseCookie
import org.springframework.stereotype.Service
import kotlin.time.Duration.Companion.milliseconds

@Service
class AccessTokenService(
private val objectMapper: ObjectMapper,
@Value("\${spring.jwt.secure}") private val secure: Boolean,
@Value("\${spring.jwt.sameSite}") private val sameSite: String,
@Value("\${spring.jwt.duration}") private val duration: Long,
@Value("\${spring.jwt.secret}") private val secret: String
) {

val COOKIE_NAME = "access_token"

private val maxAge = duration.milliseconds

private val jwtService = JwtService(secret)

fun createCookie(accessTokenClaims: AccessTokenClaims): ResponseCookie {
val claims = objectMapper.convertValue(accessTokenClaims, object : TypeReference<Map<String, *>>() {})
val jwtToken = jwtService.generateToken(JwtClaims.from(claims, maxAge))
return ResponseCookie.from(COOKIE_NAME, jwtToken)
.httpOnly(true)
.path("/")
.secure(secure)
.sameSite(sameSite)
.maxAge(maxAge.inWholeSeconds)
.build()
}

fun extractClaims(token: String): AccessTokenClaims? {
return jwtService.extractToken(token)?.let {
objectMapper.convertValue(it, object : TypeReference<AccessTokenClaims>() {})
}
}

fun createClaims(authUser: AuthUser): AccessTokenClaims {
return AccessTokenClaims(authUser)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.tul.backend.auth.base.service

import com.tul.backend.auth.base.dto.JwtClaims
import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.io.Decoders
import io.jsonwebtoken.security.Keys

class JwtService(
secret: String
) {

private val secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret))

fun generateToken(claims: JwtClaims): String {
return Jwts
.builder()
.claims()
.add(claims.claims)
.issuedAt(claims.issuedAt)
.expiration(claims.expiration)
.and()
.signWith(secretKey)
.compact()
}

fun extractToken(token: String): Claims? {
return try {
Jwts
.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.payload
} catch (e: Exception) {
null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.tul.backend.auth.base.service

import com.tul.backend.auth.base.dto.AccessTokenClaims
import jakarta.servlet.http.HttpServletRequest
import org.springframework.stereotype.Component
import org.springframework.web.util.WebUtils

@Component
class TokenFilter(
private val accessTokenService: AccessTokenService
) {

fun validateRequest(request: HttpServletRequest): AccessTokenClaims? {
val token = WebUtils.getCookie(request, accessTokenService.COOKIE_NAME)

if (token != null) {
return accessTokenService.extractClaims(token.value)
}
return null
}
}

This file was deleted.

Loading

0 comments on commit 1fe233f

Please sign in to comment.