Move user infos to JWT tokens #19

Merged
william merged 12 commits from feature/12-user-info-in-jwt into develop 2021-12-02 21:58:27 -05:00
9 changed files with 81 additions and 57 deletions
Showing only changes of commit 4dfca3349c - Show all commits

View File

@ -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 {

2
gradlew vendored
View File

@ -72,7 +72,7 @@ case "`uname`" in
Darwin* )
darwin=true
;;
MINGW* )
MSYS* | MINGW* )
msys=true
;;
NONSTOP* )

View File

@ -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

View File

@ -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
}

View File

@ -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())
}

View File

@ -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(),

View File

@ -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)
}
}

View File

@ -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())
}

View File

@ -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