Adds JWT-based auth with refresh tokens
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
package net.halfbinary.scavengerhuntapi.config
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.web.AuthenticationEntryPoint
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class AuthEntrypointJwt: AuthenticationEntryPoint {
|
||||
override fun commence(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
authException: AuthenticationException
|
||||
) {
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package net.halfbinary.scavengerhuntapi.config
|
||||
|
||||
import jakarta.servlet.FilterChain
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import net.halfbinary.scavengerhuntapi.service.HunterDetailsService
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.filter.OncePerRequestFilter
|
||||
|
||||
|
||||
@Component
|
||||
class AuthTokenFilter(private val jwtUtils: JwtUtil, private val hunterDetailsService: HunterDetailsService): OncePerRequestFilter() {
|
||||
override fun doFilterInternal(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
filterChain: FilterChain
|
||||
) {
|
||||
try {
|
||||
val jwt: String? = parseJwt(request)
|
||||
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
|
||||
val username = jwtUtils.getUsernameFromToken(jwt)
|
||||
val userDetails: UserDetails = hunterDetailsService.loadUserByUsername(username)
|
||||
val authentication =
|
||||
UsernamePasswordAuthenticationToken(
|
||||
userDetails,
|
||||
null,
|
||||
userDetails.authorities
|
||||
)
|
||||
authentication.details = WebAuthenticationDetailsSource().buildDetails(request)
|
||||
SecurityContextHolder.getContext().authentication = authentication
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Cannot set user authentication: $e")
|
||||
}
|
||||
filterChain.doFilter(request, response)
|
||||
}
|
||||
|
||||
private fun parseJwt(request: HttpServletRequest): String? {
|
||||
val headerAuth = request.getHeader("Authorization")
|
||||
if (headerAuth != null && headerAuth.startsWith("Bearer ")) {
|
||||
return headerAuth.substring(7)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package net.halfbinary.scavengerhuntapi.config
|
||||
|
||||
import io.jsonwebtoken.JwtException
|
||||
import io.jsonwebtoken.Jwts
|
||||
import jakarta.annotation.PostConstruct
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Component
|
||||
import java.util.Date
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
@Component
|
||||
class JwtUtil {
|
||||
@Value($$"${jwt.secret}")
|
||||
private val jwtSecret: String? = null
|
||||
|
||||
@Value($$"${jwt.expiration}")
|
||||
private val jwtExpirationMs = 0
|
||||
|
||||
private var key: SecretKey? = null
|
||||
|
||||
// Initializes the key after the class is instantiated and the jwtSecret is injected,
|
||||
// preventing the repeated creation of the key and enhancing performance
|
||||
@PostConstruct
|
||||
fun init() {
|
||||
this.key = Jwts.SIG.HS256.key().build()
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
fun generateToken(email: String): String {
|
||||
return Jwts.builder()
|
||||
.subject(email)
|
||||
.issuedAt(Date())
|
||||
.expiration(Date(System.currentTimeMillis() + jwtExpirationMs))
|
||||
.signWith(key)
|
||||
.compact()
|
||||
}
|
||||
|
||||
// Get username from JWT token
|
||||
fun getUsernameFromToken(token: String): String {
|
||||
return Jwts.parser()
|
||||
.verifyWith(key)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.payload
|
||||
.subject
|
||||
}
|
||||
|
||||
// Validate JWT token
|
||||
fun validateJwtToken(token: String?): Boolean {
|
||||
try {
|
||||
Jwts.parser().verifyWith(key).build().parseSignedClaims(token)
|
||||
return true
|
||||
} catch (e: SecurityException) {
|
||||
println("Invalid JWT signature: " + e.message)
|
||||
} catch (e: JwtException) {
|
||||
println("Invalid JWT token: " + e.message)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
println("JWT claims string is empty: " + e.message)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package net.halfbinary.scavengerhuntapi.config
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configurers.CorsConfigurer
|
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer
|
||||
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer
|
||||
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
|
||||
|
||||
@Configuration
|
||||
//@EnableWebSecurity
|
||||
class SecurityConfig(private val authEntrypointJwt: AuthEntrypointJwt,
|
||||
private val authTokenFilter: AuthTokenFilter) {
|
||||
|
||||
@Bean
|
||||
fun authenticationJwtTokenFilter(): AuthTokenFilter {
|
||||
return authTokenFilter
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Throws(Exception::class)
|
||||
fun authenticationManager(
|
||||
authenticationConfiguration: AuthenticationConfiguration
|
||||
): AuthenticationManager? {
|
||||
return authenticationConfiguration.getAuthenticationManager()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun passwordEncoder(): PasswordEncoder {
|
||||
return BCryptPasswordEncoder()
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Throws(Exception::class)
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
|
||||
// Updated configuration for Spring Security 6.x
|
||||
http
|
||||
.csrf { csrf: CsrfConfigurer<HttpSecurity> -> csrf.disable() } // Disable CSRF
|
||||
.cors { cors: CorsConfigurer<HttpSecurity> -> cors.disable() } // Disable CORS (or configure if needed)
|
||||
.exceptionHandling { exceptionHandling: ExceptionHandlingConfigurer<HttpSecurity> ->
|
||||
exceptionHandling.authenticationEntryPoint(
|
||||
authEntrypointJwt
|
||||
)
|
||||
}
|
||||
.sessionManagement { sessionManagement: SessionManagementConfigurer<HttpSecurity> ->
|
||||
sessionManagement.sessionCreationPolicy(
|
||||
SessionCreationPolicy.STATELESS
|
||||
)
|
||||
}
|
||||
.authorizeHttpRequests { authorizeRequests ->
|
||||
authorizeRequests
|
||||
.requestMatchers("/auth/**", "/signup")
|
||||
.permitAll()
|
||||
.anyRequest().authenticated()
|
||||
}
|
||||
|
||||
// Add the JWT Token filter before the UsernamePasswordAuthenticationFilter
|
||||
http.addFilterBefore(
|
||||
authenticationJwtTokenFilter(),
|
||||
UsernamePasswordAuthenticationFilter::class.java
|
||||
)
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user