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
4 changed files with 59 additions and 142 deletions
Showing only changes of commit 1216ace314 - Show all commits

View File

@ -3,12 +3,9 @@ package dev.fyloz.colorrecipesexplorer.config.security
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.account.User
import dev.fyloz.colorrecipesexplorer.model.account.UserDetails
import dev.fyloz.colorrecipesexplorer.model.account.UserLoginRequest
import dev.fyloz.colorrecipesexplorer.model.account.UserOutputDto
import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.utils.buildJwt
import dev.fyloz.colorrecipesexplorer.utils.parseJwt
import dev.fyloz.colorrecipesexplorer.utils.parseJwtUser
import io.jsonwebtoken.ExpiredJwtException
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
@ -30,7 +27,6 @@ class JwtAuthenticationFilter(
private val securityProperties: CreSecurityProperties,
private val updateUserLoginTime: (Long) -> Unit
) : UsernamePasswordAuthenticationFilter() {
private val objectMapper = jacksonObjectMapper()
private var debugMode = false
init {
@ -39,7 +35,7 @@ class JwtAuthenticationFilter(
}
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
val loginRequest = objectMapper.readValue(request.inputStream, UserLoginRequest::class.java)
val loginRequest = jacksonObjectMapper().readValue(request.inputStream, UserLoginRequest::class.java)
return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password))
}
@ -50,8 +46,7 @@ class JwtAuthenticationFilter(
auth: Authentication
) {
val userDetails = (auth.principal as UserDetails)
val token =
userDetails.user.buildJwt(securityProperties.jwtSecret, duration = securityProperties.jwtDuration).token
val token = userDetails.user.buildJwt(securityProperties.jwtSecret, securityProperties.jwtDuration)
var bearerCookie =
"$authorizationCookieName=Bearer$token; Max-Age=${securityProperties.jwtDuration / 1000}; HttpOnly; SameSite=strict"
@ -104,18 +99,23 @@ class JwtAuthorizationFilter(
private fun getAuthentication(token: String): UsernamePasswordAuthenticationToken? {
return try {
with(parseJwt<UserOutputDto>(token.replace("Bearer", ""), securityProperties.jwtSecret)) {
getAuthenticationToken(this.subject)
}
val user = parseJwtUser(token.replace("Bearer", ""), securityProperties.jwtSecret)
getAuthenticationToken(user)
} catch (_: ExpiredJwtException) {
null
}
}
private fun getAuthenticationToken(userId: String): UsernamePasswordAuthenticationToken? = try {
val userDetails = loadUserById(userId.toLong())
private fun getAuthenticationToken(user: UserOutputDto) =
UsernamePasswordAuthenticationToken(user.id, null, user.permissions.toAuthorities())
private fun getAuthenticationToken(userId: Long): UsernamePasswordAuthenticationToken? = try {
val userDetails = loadUserById(userId)
UsernamePasswordAuthenticationToken(userDetails.username, null, userDetails.authorities)
} catch (_: NotFoundException) {
null
}
private fun getAuthenticationToken(userId: String) =
getAuthenticationToken(userId.toLong())
}

View File

@ -7,7 +7,6 @@ import dev.fyloz.colorrecipesexplorer.model.EntityDto
import dev.fyloz.colorrecipesexplorer.model.Model
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import java.time.LocalDateTime
@ -111,9 +110,7 @@ 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 getAuthorities() = user.flatPermissions.toAuthorities()
override fun isAccountNonExpired() = true
override fun isAccountNonLocked() = true
@ -189,17 +186,20 @@ fun userUpdateDto(
op: UserUpdateDto.() -> Unit = {}
) = UserUpdateDto(id, firstName, lastName, groupId, permissions).apply(op)
fun userOutputDto(
user: User
) = UserOutputDto(
user.id,
user.firstName,
user.lastName,
user.group,
user.flatPermissions,
user.permissions,
user.lastLoginTime
)
// ==== Extensions ====
fun Set<Permission>.toAuthorities() =
this.map { it.toAuthority() }.toMutableSet()
fun User.toOutputDto() =
UserOutputDto(
this.id,
this.firstName,
this.lastName,
this.group,
this.flatPermissions,
this.permissions,
this.lastLoginTime
)
// ==== Exceptions ====
private const val USER_NOT_FOUND_EXCEPTION_TITLE = "User not found"

View File

@ -62,7 +62,7 @@ class UserServiceImpl(
override fun idNotFoundException(id: Long) = userIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = userIdAlreadyExistsException(id)
override fun User.toOutput() = userOutputDto(this)
override fun User.toOutput() = this.toOutputDto()
override fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean =
repository.existsByFirstNameAndLastName(firstName, lastName)

View File

@ -1,139 +1,56 @@
package dev.fyloz.colorrecipesexplorer.utils
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.TreeNode
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
import com.fasterxml.jackson.module.kotlin.readValue
import dev.fyloz.colorrecipesexplorer.model.account.User
import dev.fyloz.colorrecipesexplorer.model.account.userOutputDto
import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jws
import dev.fyloz.colorrecipesexplorer.model.account.UserOutputDto
import dev.fyloz.colorrecipesexplorer.model.account.toOutputDto
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.io.DeserializationException
import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.io.IOException
import io.jsonwebtoken.jackson.io.JacksonDeserializer
import io.jsonwebtoken.jackson.io.JacksonSerializer
import io.jsonwebtoken.security.Keys
import java.util.*
import javax.crypto.SecretKey
private const val userClaimName = "user"
data class Jwt<B>(
val subject: String,
val secret: String,
val duration: Long? = null,
val signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.HS512,
val body: B? = null
) {
val token: String by lazy {
val builder = Jwts.builder()
.signWith(keyFromSecret(secret))
.setSubject(subject)
.claim("payload", body)
private val objectMapper = jacksonMapperBuilder()
.apply { addModule(JavaTimeModule()) }
.build()
private val serializer = JacksonSerializer<Map<String, *>>(objectMapper)
private val deserializer = JacksonDeserializer<Map<String, *>>(objectMapper)
duration?.let {
val expirationMs = System.currentTimeMillis() + it
val expirationDate = Date(expirationMs)
/** Build a JWT token for the given [User]. */
fun User.buildJwt(secret: String, duration: Long): String {
val expirationDate = Date(System.currentTimeMillis() + duration)
val serializedUser = objectMapper.writeValueAsString(this.toOutputDto())
builder.setExpiration(expirationDate)
}
builder.compact()
}
return Jwts.builder()
.serializeToJsonWith(serializer)
.signWith(keyFromSecret(secret))
.setSubject(this.id.toString())
.setExpiration(expirationDate)
.claim(userClaimName, serializedUser)
.compact()
}
enum class ClaimType(val key: String) {
GROUP_ID("groupId"),
GROUP_NAME("groupName")
}
data class UserJwtBody(
val groupId: Long?,
val groupName: String?
)
/** Build a [Jwt] for the given [User]. */
fun User.buildJwt(secret: String, duration: Long?) =
Jwt(
subject = this.id.toString(),
secret,
duration,
body = userOutputDto(this)
)
//class JacksonDeserializer<T>(
// val claimTypeMap: Map<String, Class<*>>
//) : Deserializer<T> {
// val objectMapper: ObjectMapper
//
// init {
// objectMapper = jacksonObjectMapper()
//
// val module = SimpleModule()
// module.addDeserializer(Any::class.java, MappedTypeDeserializer(Collections.unmodifiableMap(claimTypeMap)))
// objectMapper.registerModule(module)
// }
//
// override fun deserialize(bytes: ByteArray?): T {
// return try {
// readValue(bytes)
// } catch (e: IOException) {
// val msg =
// "Unable to deserialize bytes into a " + returnType.getName().toString() + " instance: " + e.getMessage()
// throw DeserializationException(msg, e)
// }
// }
//
// protected fun readValue(bytes: ByteArray?): T {
// return objectMapper.readValue(bytes, returnType)
// }
//}
//
//private class MappedTypeDeserializer(
// private val claimTypeMap: Map<String, Class<*>>
//) : UntypedObjectDeserializer(null, null) {
// override fun deserialize(parser: JsonParser, context: DeserializationContext): Any {
// val name: String = parser.currentName()
// if (claimTypeMap.containsKey(name)) {
// val type = claimTypeMap[name]!!
// return parser.readValueAsTree<TreeNode>().traverse(parser.codec).readValueAs(type)
// }
// // otherwise default to super
// return super.deserialize(parser, context)
// }
//}
class CustomDeserializer<T>(map: Map<String, Class<*>>) {
private val objectMapper: ObjectMapper = jacksonObjectMapper()
private val returnType: Class<T> = Object::class.java as Class<T>
}
/** Parses the given [jwt] string. */
inline fun <reified B> parseJwt(jwt: String, secret: String) =
/** Parses the user of the given [jwt]. */
fun parseJwtUser(jwt: String, secret: String): UserOutputDto =
with(
Jwts.parserBuilder()
.deserializeJsonWith(JacksonDeserializer(mapOf("payload" to B::class.java)))
.deserializeJsonWith(deserializer)
.setSigningKey(keyFromSecret(secret))
.build()
.parseClaimsJws(jwt)
.body.get(userClaimName, String::class.java)
) {
val jwt = Jwt<B>(this.body.subject, secret)
val payload = this.body.get("payload", B::class.java)
jwt
objectMapper.readValue(this)
}
/** Creates a base64 encoded [SecretKey] from the given [secret]. */
fun keyFromSecret(secret: String) =
private fun keyFromSecret(secret: String): SecretKey =
with(Encoders.BASE64.encode(secret.toByteArray())) {
Keys.hmacShaKeyFor(this.toByteArray())
}
/** Gets the claim with the given [claimType] in a [Jws]. */
private inline fun <reified T> Jws<Claims>.getClaim(claimType: ClaimType) =
this.body.get(claimType.key, T::class.java)