Move user infos to JWT tokens #19
|
@ -60,6 +60,7 @@ dependencies {
|
|||
runtimeOnly("com.microsoft.sqlserver:mssql-jdbc:9.2.1.jre11")
|
||||
|
||||
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2")
|
||||
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2")
|
||||
}
|
||||
|
||||
springBoot {
|
||||
|
|
|
@ -72,7 +72,7 @@ case "`uname`" in
|
|||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
MSYS* | MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
package dev.fyloz.colorrecipesexplorer
|
||||
|
||||
public typealias SpringUser = org.springframework.security.core.userdetails.User
|
||||
typealias SpringUser = org.springframework.security.core.userdetails.User
|
||||
typealias SpringUserDetails = org.springframework.security.core.userdetails.UserDetails
|
||||
typealias SpringUserDetailsService = org.springframework.security.core.userdetails.UserDetailsService
|
||||
|
|
|
@ -1,21 +1,19 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.security
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import dev.fyloz.colorrecipesexplorer.SpringUser
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.UserLoginRequest
|
||||
import dev.fyloz.colorrecipesexplorer.utils.buildJwt
|
||||
import dev.fyloz.colorrecipesexplorer.utils.parseJwt
|
||||
import io.jsonwebtoken.ExpiredJwtException
|
||||
import io.jsonwebtoken.Jwts
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
|
||||
import org.springframework.util.Assert
|
||||
import org.springframework.web.util.WebUtils
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
@ -46,10 +44,11 @@ class JwtAuthenticationFilter(
|
|||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
chain: FilterChain,
|
||||
authResult: Authentication
|
||||
auth: Authentication
|
||||
) {
|
||||
val user = (authResult.principal as SpringUser)
|
||||
val token = user.buildJwt(securityProperties)
|
||||
val userDetails = (auth.principal as UserDetails)
|
||||
val token =
|
||||
userDetails.user.buildJwt(securityProperties.jwtSecret, duration = securityProperties.jwtDuration).token
|
||||
|
||||
var bearerCookie =
|
||||
"$authorizationCookieName=Bearer$token; Max-Age=${securityProperties.jwtDuration / 1000}; HttpOnly; SameSite=strict"
|
||||
|
@ -59,11 +58,13 @@ class JwtAuthenticationFilter(
|
|||
bearerCookie
|
||||
)
|
||||
response.addHeader(authorizationCookieName, "Bearer $token")
|
||||
|
||||
updateUserLoginTime(userDetails.user.id)
|
||||
}
|
||||
}
|
||||
|
||||
class JwtAuthorizationFilter(
|
||||
private val securityConfigurationProperties: CreSecurityProperties,
|
||||
private val securityProperties: CreSecurityProperties,
|
||||
authenticationManager: AuthenticationManager,
|
||||
private val loadUserById: (Long) -> UserDetails
|
||||
) : BasicAuthenticationFilter(authenticationManager) {
|
||||
|
@ -99,16 +100,10 @@ class JwtAuthorizationFilter(
|
|||
}
|
||||
|
||||
private fun getAuthentication(token: String): UsernamePasswordAuthenticationToken? {
|
||||
val jwtSecret = securityConfigurationProperties.jwtSecret
|
||||
Assert.notNull(jwtSecret, "No JWT secret has been defined.")
|
||||
return try {
|
||||
|
||||
val userId = Jwts.parser()
|
||||
.setSigningKey(jwtSecret.toByteArray())
|
||||
.parseClaimsJws(token.replace("Bearer", ""))
|
||||
.body
|
||||
.subject
|
||||
if (userId != null) getAuthenticationToken(userId) else null
|
||||
with(parseJwt(token.replace("Bearer", ""), securityProperties.jwtSecret)) {
|
||||
getAuthenticationToken(this.subject)
|
||||
}
|
||||
} catch (_: ExpiredJwtException) {
|
||||
null
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
|||
import dev.fyloz.colorrecipesexplorer.emergencyMode
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.User
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.user
|
||||
import dev.fyloz.colorrecipesexplorer.service.CreUserDetailsService
|
||||
import dev.fyloz.colorrecipesexplorer.service.UserService
|
||||
import org.slf4j.Logger
|
||||
|
@ -22,7 +24,6 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
|
|||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
|
@ -34,7 +35,6 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
|||
import javax.annotation.PostConstruct
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import org.springframework.security.core.userdetails.User as SpringUser
|
||||
|
||||
@Configuration
|
||||
@Profile("!emergency")
|
||||
|
@ -122,8 +122,6 @@ class EmergencySecurityConfig(
|
|||
private val securityProperties: CreSecurityProperties,
|
||||
private val environment: Environment
|
||||
) : WebSecurityConfigurerAdapter() {
|
||||
private val rootUserRole = Permission.ADMIN.name
|
||||
|
||||
init {
|
||||
emergencyMode = true
|
||||
}
|
||||
|
@ -142,7 +140,7 @@ class EmergencySecurityConfig(
|
|||
auth.inMemoryAuthentication()
|
||||
.withUser(securityProperties.root!!.id.toString())
|
||||
.password(passwordEncoder().encode(securityProperties.root!!.password))
|
||||
.authorities(SimpleGrantedAuthority(rootUserRole))
|
||||
.authorities(SimpleGrantedAuthority(Permission.ADMIN.name))
|
||||
}
|
||||
|
||||
override fun configure(http: HttpSecurity) {
|
||||
|
@ -172,11 +170,11 @@ class EmergencySecurityConfig(
|
|||
private fun loadUserById(id: Long): UserDetails {
|
||||
assertRootUserNotNull(securityProperties)
|
||||
if (id == securityProperties.root!!.id) {
|
||||
return SpringUser(
|
||||
id.toString(),
|
||||
securityProperties.root!!.password,
|
||||
listOf(SimpleGrantedAuthority(rootUserRole))
|
||||
)
|
||||
return UserDetails(user(
|
||||
id = id,
|
||||
password = securityProperties.root!!.password,
|
||||
permissions = mutableSetOf(Permission.ADMIN)
|
||||
))
|
||||
}
|
||||
throw UsernameNotFoundException(id.toString())
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package dev.fyloz.colorrecipesexplorer.model.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.SpringUserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.EntityDto
|
||||
|
@ -59,9 +60,6 @@ data class User(
|
|||
.apply {
|
||||
if (group != null) this.addAll(group!!.flatPermissions)
|
||||
}
|
||||
|
||||
val authorities: Set<GrantedAuthority>
|
||||
get() = flatPermissions.map { it.toAuthority() }.toMutableSet()
|
||||
}
|
||||
|
||||
open class UserSaveDto(
|
||||
|
@ -110,6 +108,19 @@ data class UserOutputDto(
|
|||
|
||||
data class UserLoginRequest(val id: Long, val password: String)
|
||||
|
||||
data class UserDetails(val user: User) : SpringUserDetails {
|
||||
override fun getPassword() = user.password
|
||||
override fun getUsername() = user.id.toString()
|
||||
|
||||
override fun getAuthorities() =
|
||||
user.flatPermissions.map { it.toAuthority() }.toMutableSet()
|
||||
|
||||
override fun isAccountNonExpired() = true
|
||||
override fun isAccountNonLocked() = true
|
||||
override fun isCredentialsNonExpired() = true
|
||||
override fun isEnabled() = true
|
||||
}
|
||||
|
||||
// ==== DSL ====
|
||||
fun user(
|
||||
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.SpringUserDetailsService
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.blacklistedJwtTokens
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
|
@ -9,8 +10,6 @@ import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
|
|||
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Service
|
||||
|
@ -19,7 +18,6 @@ import java.time.LocalDateTime
|
|||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import javax.transaction.Transactional
|
||||
import org.springframework.security.core.userdetails.User as SpringUser
|
||||
|
||||
interface UserService :
|
||||
ExternalModelService<User, UserSaveDto, UserUpdateDto, UserOutputDto, UserRepository> {
|
||||
|
@ -69,7 +67,7 @@ interface GroupService :
|
|||
fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse)
|
||||
}
|
||||
|
||||
interface CreUserDetailsService : UserDetailsService {
|
||||
interface CreUserDetailsService : SpringUserDetailsService {
|
||||
/** Loads an [User] for the given [id]. */
|
||||
fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails
|
||||
}
|
||||
|
@ -304,8 +302,7 @@ class GroupServiceImpl(
|
|||
@Profile("!emergency")
|
||||
class CreUserDetailsServiceImpl(
|
||||
private val userService: UserService
|
||||
) :
|
||||
CreUserDetailsService {
|
||||
) : CreUserDetailsService {
|
||||
override fun loadUserByUsername(username: String): UserDetails {
|
||||
try {
|
||||
return loadUserById(username.toLong(), true)
|
||||
|
@ -322,6 +319,6 @@ class CreUserDetailsServiceImpl(
|
|||
ignoreDefaultGroupUsers = ignoreDefaultGroupUsers,
|
||||
ignoreSystemUsers = false
|
||||
)
|
||||
return SpringUser(user.id.toString(), user.password, user.authorities)
|
||||
return UserDetails(user)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +1,53 @@
|
|||
package dev.fyloz.colorrecipesexplorer.utils
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.SpringUser
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.User
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.SignatureAlgorithm
|
||||
import io.jsonwebtoken.io.Encoders
|
||||
import io.jsonwebtoken.security.Keys
|
||||
import java.util.*
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
data class Jwt(
|
||||
val subject: String,
|
||||
val secret: String,
|
||||
val duration: Long,
|
||||
val duration: Long? = null,
|
||||
val signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.HS512
|
||||
)
|
||||
) {
|
||||
val token: String by lazy {
|
||||
val builder = Jwts.builder()
|
||||
.setSubject(subject)
|
||||
|
||||
fun SpringUser.buildJwt(properties: CreSecurityProperties) =
|
||||
Jwt(this.username, properties.jwtSecret, properties.jwtDuration).build()
|
||||
duration?.let {
|
||||
val expirationMs = System.currentTimeMillis() + it
|
||||
val expirationDate = Date(expirationMs)
|
||||
|
||||
fun Jwt.build(): String {
|
||||
val expirationMs = System.currentTimeMillis() + this.duration
|
||||
val expirationDate = Date(expirationMs)
|
||||
builder.setExpiration(expirationDate)
|
||||
}
|
||||
|
||||
val base64Secret = Encoders.BASE64.encode(this.secret.toByteArray())
|
||||
val key = Keys.hmacShaKeyFor(base64Secret.toByteArray())
|
||||
|
||||
return Jwts.builder()
|
||||
.setSubject(this.subject)
|
||||
.setExpiration(expirationDate)
|
||||
.signWith(key)
|
||||
.compact()
|
||||
builder
|
||||
.signWith(keyFromSecret(secret))
|
||||
.compact()
|
||||
}
|
||||
}
|
||||
|
||||
/** Build a [Jwt] for the given [User]. */
|
||||
fun User.buildJwt(secret: String, duration: Long?) =
|
||||
Jwt(this.id.toString(), secret, duration)
|
||||
|
||||
/** Parses the given [jwt] string. */
|
||||
fun parseJwt(jwt: String, secret: String) =
|
||||
with(
|
||||
Jwts.parserBuilder()
|
||||
.setSigningKey(keyFromSecret(secret))
|
||||
.build()
|
||||
.parseClaimsJws(jwt)
|
||||
) {
|
||||
Jwt(this.body.subject, secret)
|
||||
}
|
||||
|
||||
/** Creates a base64 encoded [SecretKey] from the given [secret]. */
|
||||
private fun keyFromSecret(secret: String) =
|
||||
with(Encoders.BASE64.encode(secret.toByteArray())) {
|
||||
Keys.hmacShaKeyFor(this.toByteArray())
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ server.port=9090
|
|||
# CRE
|
||||
cre.server.data-directory=data
|
||||
cre.server.config-directory=config
|
||||
cre.security.jwt-secret=CtnvGQjgZ44A1fh295gE
|
||||
cre.security.jwt-secret=CtnvGQjgZ44A1fh295gE78WWOgl8InrbwBgQsMy0
|
||||
cre.security.jwt-duration=18000000
|
||||
cre.security.aes-secret=blabla
|
||||
# Root user
|
||||
|
|
Loading…
Reference in New Issue