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
7 changed files with 102 additions and 31 deletions
Showing only changes of commit 72f9710bdb - Show all commits

View File

@ -30,7 +30,7 @@ dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom:${kotlinVersion}"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.3")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.4")
implementation("javax.xml.bind:jaxb-api:2.3.0")
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
implementation("io.jsonwebtoken:jjwt-impl:0.11.2")

View File

@ -3,8 +3,10 @@ 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.utils.buildJwt
import dev.fyloz.colorrecipesexplorer.utils.parseJwt
import io.jsonwebtoken.ExpiredJwtException
@ -28,6 +30,7 @@ class JwtAuthenticationFilter(
private val securityProperties: CreSecurityProperties,
private val updateUserLoginTime: (Long) -> Unit
) : UsernamePasswordAuthenticationFilter() {
private val objectMapper = jacksonObjectMapper()
private var debugMode = false
init {
@ -36,7 +39,7 @@ class JwtAuthenticationFilter(
}
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
val loginRequest = jacksonObjectMapper().readValue(request.inputStream, UserLoginRequest::class.java)
val loginRequest = objectMapper.readValue(request.inputStream, UserLoginRequest::class.java)
return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password))
}
@ -101,7 +104,7 @@ class JwtAuthorizationFilter(
private fun getAuthentication(token: String): UsernamePasswordAuthenticationToken? {
return try {
with(parseJwt(token.replace("Bearer", ""), securityProperties.jwtSecret)) {
with(parseJwt<UserOutputDto>(token.replace("Bearer", ""), securityProperties.jwtSecret)) {
getAuthenticationToken(this.subject)
}
} catch (_: ExpiredJwtException) {

View File

@ -189,6 +189,18 @@ 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
)
// ==== Exceptions ====
private const val USER_NOT_FOUND_EXCEPTION_TITLE = "User not found"
private const val USER_ALREADY_EXISTS_EXCEPTION_TITLE = "User already exists"

View File

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

View File

@ -1,26 +1,40 @@
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 dev.fyloz.colorrecipesexplorer.model.account.User
import dev.fyloz.colorrecipesexplorer.model.account.userOutputDto
import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jws
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.security.Keys
import java.util.*
import javax.crypto.SecretKey
data class Jwt(
data class Jwt<B>(
val subject: String,
val secret: String,
val duration: Long? = null,
val signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.HS512,
val claims: Map<JwtClaim, Any?> = mapOf()
val body: B? = null
) {
val token: String by lazy {
val builder = Jwts.builder()
.signWith(keyFromSecret(secret))
.setSubject(subject)
.addClaims(mappedClaims)
.claim("payload", body)
duration?.let {
val expirationMs = System.currentTimeMillis() + it
@ -31,45 +45,95 @@ data class Jwt(
builder.compact()
}
private val mappedClaims: Map<String, String> by lazy {
claims
.filterValues { it != null }
.mapKeys { it.key.claim }
.mapValues { it.value.toString() }
}
}
enum class JwtClaim(val claim: String) {
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,
claims = mapOf(
JwtClaim.GROUP_ID to this.group?.id,
JwtClaim.GROUP_NAME to this.group?.name
)
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. */
fun parseJwt(jwt: String, secret: String) =
inline fun <reified B> parseJwt(jwt: String, secret: String) =
with(
Jwts.parserBuilder()
.deserializeJsonWith(JacksonDeserializer(mapOf("payload" to B::class.java)))
.setSigningKey(keyFromSecret(secret))
.build()
.parseClaimsJws(jwt)
) {
Jwt(this.body.subject, secret)
val jwt = Jwt<B>(this.body.subject, secret)
val payload = this.body.get("payload", B::class.java)
jwt
}
/** Creates a base64 encoded [SecretKey] from the given [secret]. */
private fun keyFromSecret(secret: String) =
fun keyFromSecret(secret: String) =
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)

View File

@ -19,7 +19,6 @@ import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import org.springframework.security.core.userdetails.User as SpringUser
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserServiceTest :

View File

@ -6,6 +6,7 @@ import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.account.group
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService
import dev.fyloz.colorrecipesexplorer.service.users.GroupService
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test