Compare commits

..

No commits in common. "72f9710bdb35bfec1fc8852379c5d5de68fb758e" and "975ebae553465398091b00377cf8790c80ea7547" have entirely different histories.

12 changed files with 198 additions and 342 deletions

View File

@ -30,11 +30,9 @@ 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.12.4")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.3")
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")
implementation("io.jsonwebtoken:jjwt-jackson:0.11.2")
implementation("org.apache.poi:poi-ooxml:4.1.0")
implementation("org.apache.pdfbox:pdfbox:2.0.4")
implementation("dev.fyloz.colorrecipesexplorer:database-manager:5.2")
@ -60,6 +58,9 @@ dependencies {
runtimeOnly("mysql:mysql-connector-java:8.0.22")
runtimeOnly("org.postgresql:postgresql:42.2.16")
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 {

View File

@ -3,10 +3,8 @@ 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
@ -30,7 +28,6 @@ class JwtAuthenticationFilter(
private val securityProperties: CreSecurityProperties,
private val updateUserLoginTime: (Long) -> Unit
) : UsernamePasswordAuthenticationFilter() {
private val objectMapper = jacksonObjectMapper()
private var debugMode = false
init {
@ -39,7 +36,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))
}
@ -104,7 +101,7 @@ class JwtAuthorizationFilter(
private fun getAuthentication(token: String): UsernamePasswordAuthenticationToken? {
return try {
with(parseJwt<UserOutputDto>(token.replace("Bearer", ""), securityProperties.jwtSecret)) {
with(parseJwt(token.replace("Bearer", ""), securityProperties.jwtSecret)) {
getAuthenticationToken(this.subject)
}
} catch (_: ExpiredJwtException) {

View File

@ -4,8 +4,10 @@ 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.service.users.UserDetailsService
import dev.fyloz.colorrecipesexplorer.service.users.UserService
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
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
@ -21,6 +23,8 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
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.UsernameNotFoundException
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.AuthenticationEntryPoint
@ -39,7 +43,7 @@ import javax.servlet.http.HttpServletResponse
@EnableConfigurationProperties(CreSecurityProperties::class)
class SecurityConfig(
private val securityProperties: CreSecurityProperties,
@Lazy private val userDetailsService: UserDetailsService,
@Lazy private val userDetailsService: CreUserDetailsService,
@Lazy private val userService: UserService,
private val environment: Environment,
private val logger: Logger
@ -116,7 +120,6 @@ class SecurityConfig(
@EnableConfigurationProperties(CreSecurityProperties::class)
class EmergencySecurityConfig(
private val securityProperties: CreSecurityProperties,
private val userDetailsService: UserDetailsService,
private val environment: Environment
) : WebSecurityConfigurerAdapter() {
init {
@ -131,8 +134,13 @@ class EmergencySecurityConfig(
fun passwordEncoder() =
getPasswordEncoder()
override fun configure(authBuilder: AuthenticationManagerBuilder) {
authBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder())
override fun configure(auth: AuthenticationManagerBuilder) {
assertRootUserNotNull(securityProperties)
// Create in-memory root user
auth.inMemoryAuthentication()
.withUser(securityProperties.root!!.id.toString())
.password(passwordEncoder().encode(securityProperties.root!!.password))
.authorities(SimpleGrantedAuthority(Permission.ADMIN.name))
}
override fun configure(http: HttpSecurity) {
@ -146,9 +154,7 @@ class EmergencySecurityConfig(
JwtAuthenticationFilter(authenticationManager(), securityProperties) { }
)
.addFilter(
JwtAuthorizationFilter(securityProperties, authenticationManager()) {
userDetailsService.loadUserById(it, false)
}
JwtAuthorizationFilter(securityProperties, authenticationManager(), this::loadUserById)
)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
@ -160,6 +166,18 @@ class EmergencySecurityConfig(
http.cors()
}
}
private fun loadUserById(id: Long): UserDetails {
assertRootUserNotNull(securityProperties)
if (id == securityProperties.root!!.id) {
return UserDetails(user(
id = id,
password = securityProperties.root!!.password,
permissions = mutableSetOf(Permission.ADMIN)
))
}
throw UsernameNotFoundException(id.toString())
}
}
@Component

View File

@ -123,10 +123,11 @@ data class UserDetails(val user: User) : SpringUserDetails {
// ==== DSL ====
fun user(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = "password",
password: String = passwordEncoder.encode("password"),
isDefaultGroupUser: Boolean = false,
isSystemUser: Boolean = false,
group: Group? = null,
@ -145,30 +146,6 @@ fun user(
lastLoginTime
).apply(op)
fun user(
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
plainPassword: String = "password",
isDefaultGroupUser: Boolean = false,
isSystemUser: Boolean = false,
group: Group? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
lastLoginTime: LocalDateTime? = null,
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
op: User.() -> Unit = {}
) = User(
id,
firstName,
lastName,
passwordEncoder.encode(plainPassword),
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
).apply(op)
fun userSaveDto(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
@ -189,18 +166,6 @@ 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

@ -3,8 +3,8 @@ package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers
import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.service.users.GroupService
import dev.fyloz.colorrecipesexplorer.service.users.UserService
import dev.fyloz.colorrecipesexplorer.service.UserService
import dev.fyloz.colorrecipesexplorer.service.GroupService
import org.springframework.context.annotation.Profile
import org.springframework.http.MediaType
import org.springframework.security.access.prepost.PreAuthorize

View File

@ -1,18 +1,23 @@
package dev.fyloz.colorrecipesexplorer.service.users
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
import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.model.validation.or
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
import dev.fyloz.colorrecipesexplorer.service.AbstractExternalModelService
import dev.fyloz.colorrecipesexplorer.service.ExternalModelService
import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Profile
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.web.util.WebUtils
import java.time.LocalDateTime
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import javax.transaction.Transactional
interface UserService :
ExternalModelService<User, UserSaveDto, UserUpdateDto, UserOutputDto, UserRepository> {
@ -50,11 +55,29 @@ interface UserService :
fun logout(request: HttpServletRequest)
}
interface GroupService :
ExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository> {
/** Gets all the users of the group with the given [id]. */
fun getUsersForGroup(id: Long): Collection<User>
/** Gets the default group from a cookie in the given HTTP [request]. */
fun getRequestDefaultGroup(request: HttpServletRequest): Group
/** Sets the default group cookie for the given HTTP [response]. */
fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse)
}
interface CreUserDetailsService : SpringUserDetailsService {
/** Loads an [User] for the given [id]. */
fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails
}
@Service
@Profile("!emergency")
class UserServiceImpl(
userRepository: UserRepository,
@Lazy val groupService: GroupService,
@Lazy val passwordEncoder: PasswordEncoder,
) : AbstractExternalModelService<User, UserSaveDto, UserUpdateDto, UserOutputDto, UserRepository>(
userRepository
),
@ -62,7 +85,15 @@ 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() = UserOutputDto(
this.id,
this.firstName,
this.lastName,
this.group,
this.flatPermissions,
this.permissions,
this.lastLoginTime
)
override fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean =
repository.existsByFirstNameAndLastName(firstName, lastName)
@ -89,11 +120,11 @@ class UserServiceImpl(
override fun save(entity: UserSaveDto): User =
save(with(entity) {
user(
id = id,
firstName = firstName,
lastName = lastName,
plainPassword = password,
User(
id,
firstName,
lastName,
passwordEncoder.encode(password),
isDefaultGroupUser = false,
isSystemUser = false,
group = if (groupId != null) groupService.getById(groupId) else null,
@ -115,7 +146,7 @@ class UserServiceImpl(
id = 1000000L + group.id!!,
firstName = group.name,
lastName = "User",
plainPassword = group.name,
password = passwordEncoder.encode(group.name),
group = group,
isDefaultGroupUser = true
)
@ -164,11 +195,11 @@ class UserServiceImpl(
override fun updatePassword(id: Long, password: String): User {
val persistedUser = getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
return super.update(with(persistedUser) {
user(
User(
id,
firstName,
lastName,
plainPassword = password,
passwordEncoder.encode(password),
isDefaultGroupUser,
isSystemUser,
group,
@ -194,3 +225,100 @@ class UserServiceImpl(
}
}
}
const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans
@Service
@Profile("!emergency")
class GroupServiceImpl(
private val userService: UserService,
groupRepository: GroupRepository
) : AbstractExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository>(
groupRepository
),
GroupService {
override fun idNotFoundException(id: Long) = groupIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = groupIdAlreadyExistsException(id)
override fun nameNotFoundException(name: String) = groupNameNotFoundException(name)
override fun nameAlreadyExistsException(name: String) = groupNameAlreadyExistsException(name)
override fun Group.toOutput() = GroupOutputDto(
this.id!!,
this.name,
this.permissions,
this.flatPermissions
)
override fun existsByName(name: String): Boolean = repository.existsByName(name)
override fun getUsersForGroup(id: Long): Collection<User> =
userService.getByGroup(getById(id))
@Transactional
override fun save(entity: Group): Group {
return super<AbstractExternalNamedModelService>.save(entity).apply {
userService.saveDefaultGroupUser(this)
}
}
override fun update(entity: GroupUpdateDto): Group {
val persistedGroup by lazy { getById(entity.id) }
return update(with(entity) {
Group(
entity.id,
if (name.isNotBlank()) entity.name else persistedGroup.name,
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions
)
})
}
@Transactional
override fun delete(entity: Group) {
userService.delete(userService.getDefaultGroupUser(entity))
super.delete(entity)
}
override fun getRequestDefaultGroup(request: HttpServletRequest): Group {
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
?: throw NoDefaultGroupException()
val defaultGroupUser = userService.getById(
defaultGroupCookie.value.toLong(),
ignoreDefaultGroupUsers = false,
ignoreSystemUsers = true
)
return defaultGroupUser.group!!
}
override fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) {
val group = getById(groupId)
val defaultGroupUser = userService.getDefaultGroupUser(group)
response.addHeader(
"Set-Cookie",
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=${defaultGroupCookieMaxAge}; Path=/api; HttpOnly; Secure; SameSite=strict"
)
}
}
@Service
@Profile("!emergency")
class CreUserDetailsServiceImpl(
private val userService: UserService
) : CreUserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
try {
return loadUserById(username.toLong(), true)
} catch (ex: NotFoundException) {
throw UsernameNotFoundException(username)
} catch (ex: NotFoundException) {
throw UsernameNotFoundException(username)
}
}
override fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean): UserDetails {
val user = userService.getById(
id,
ignoreDefaultGroupUsers = ignoreDefaultGroupUsers,
ignoreSystemUsers = false
)
return UserDetails(user)
}
}

View File

@ -5,7 +5,6 @@ import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.model.validation.or
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService
import dev.fyloz.colorrecipesexplorer.service.users.GroupService
import dev.fyloz.colorrecipesexplorer.utils.setAll
import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Profile

View File

@ -1,97 +0,0 @@
package dev.fyloz.colorrecipesexplorer.service.users
import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName
import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
import dev.fyloz.colorrecipesexplorer.service.AbstractExternalNamedModelService
import dev.fyloz.colorrecipesexplorer.service.ExternalNamedModelService
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Service
import org.springframework.web.util.WebUtils
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import javax.transaction.Transactional
const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans
interface GroupService :
ExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository> {
/** Gets all the users of the group with the given [id]. */
fun getUsersForGroup(id: Long): Collection<User>
/** Gets the default group from a cookie in the given HTTP [request]. */
fun getRequestDefaultGroup(request: HttpServletRequest): Group
/** Sets the default group cookie for the given HTTP [response]. */
fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse)
}
@Service
@Profile("!emergency")
class GroupServiceImpl(
private val userService: UserService,
groupRepository: GroupRepository
) : AbstractExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository>(
groupRepository
),
GroupService {
override fun idNotFoundException(id: Long) = groupIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = groupIdAlreadyExistsException(id)
override fun nameNotFoundException(name: String) = groupNameNotFoundException(name)
override fun nameAlreadyExistsException(name: String) = groupNameAlreadyExistsException(name)
override fun Group.toOutput() = GroupOutputDto(
this.id!!,
this.name,
this.permissions,
this.flatPermissions
)
override fun existsByName(name: String): Boolean = repository.existsByName(name)
override fun getUsersForGroup(id: Long): Collection<User> =
userService.getByGroup(getById(id))
@Transactional
override fun save(entity: Group): Group {
return super<AbstractExternalNamedModelService>.save(entity).apply {
userService.saveDefaultGroupUser(this)
}
}
override fun update(entity: GroupUpdateDto): Group {
val persistedGroup by lazy { getById(entity.id) }
return update(with(entity) {
Group(
entity.id,
if (name.isNotBlank()) entity.name else persistedGroup.name,
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions
)
})
}
@Transactional
override fun delete(entity: Group) {
userService.delete(userService.getDefaultGroupUser(entity))
super.delete(entity)
}
override fun getRequestDefaultGroup(request: HttpServletRequest): Group {
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
?: throw NoDefaultGroupException()
val defaultGroupUser = userService.getById(
defaultGroupCookie.value.toLong(),
ignoreDefaultGroupUsers = false,
ignoreSystemUsers = true
)
return defaultGroupUser.group!!
}
override fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) {
val group = getById(groupId)
val defaultGroupUser = userService.getDefaultGroupUser(group)
response.addHeader(
"Set-Cookie",
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=$defaultGroupCookieMaxAge; Path=/api; HttpOnly; Secure; SameSite=strict"
)
}
}

View File

@ -1,77 +0,0 @@
package dev.fyloz.colorrecipesexplorer.service.users
import dev.fyloz.colorrecipesexplorer.SpringUserDetails
import dev.fyloz.colorrecipesexplorer.SpringUserDetailsService
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
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 org.springframework.context.annotation.Profile
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Service
interface UserDetailsService : SpringUserDetailsService {
/** Loads an [User] for the given [id]. */
fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails
}
@Service
@Profile("!emergency")
class UserDetailsServiceImpl(
private val userService: UserService
) : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
try {
return loadUserById(username.toLong(), true)
} catch (ex: NotFoundException) {
throw UsernameNotFoundException(username)
}
}
override fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean): UserDetails {
val user = userService.getById(
id,
ignoreDefaultGroupUsers = ignoreDefaultGroupUsers,
ignoreSystemUsers = false
)
return UserDetails(user)
}
}
@Service
@Profile("emergency")
class EmergencyUserDetailsServiceImpl(
securityProperties: CreSecurityProperties
) : UserDetailsService {
private val users: Set<User>
init {
if (securityProperties.root == null) {
throw NullPointerException("The root user has not been configured")
}
users = setOf(
// Add root user
with(securityProperties.root!!) {
user(
id = this.id,
plainPassword = this.password,
permissions = mutableSetOf(Permission.ADMIN)
)
}
)
}
override fun loadUserByUsername(username: String): SpringUserDetails {
return loadUserById(username.toLong(), true)
}
override fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean): UserDetails {
val user = users.firstOrNull { it.id == id }
?: throw UsernameNotFoundException(id.toString())
return UserDetails(user)
}
}

View File

@ -1,40 +1,25 @@
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<B>(
data class Jwt(
val subject: String,
val secret: String,
val duration: Long? = null,
val signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.HS512,
val body: B? = null
val claims: Map<String, Any?> = mapOf()
) {
val token: String by lazy {
val builder = Jwts.builder()
.signWith(keyFromSecret(secret))
.setSubject(subject)
.claim("payload", body)
.addClaims(claims.filterValues { it != null })
duration?.let {
val expirationMs = System.currentTimeMillis() + it
@ -47,93 +32,31 @@ data class Jwt<B>(
}
}
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)
claims = mapOf(
"groupId" to this.group?.id,
"groupName" to this.group?.name
)
)
//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) =
fun parseJwt(jwt: String, secret: String) =
with(
Jwts.parserBuilder()
.deserializeJsonWith(JacksonDeserializer(mapOf("payload" to B::class.java)))
.setSigningKey(keyFromSecret(secret))
.build()
.parseClaimsJws(jwt)
) {
val jwt = Jwt<B>(this.body.subject, secret)
val payload = this.body.get("payload", B::class.java)
jwt
Jwt(this.body.subject, secret)
}
/** Creates a base64 encoded [SecretKey] from the given [secret]. */
fun keyFromSecret(secret: String) =
private 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

@ -5,9 +5,8 @@ import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
import dev.fyloz.colorrecipesexplorer.service.users.*
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
import org.junit.jupiter.api.*
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.security.core.userdetails.UsernameNotFoundException
@ -19,23 +18,24 @@ 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 :
AbstractExternalModelServiceTest<User, UserSaveDto, UserUpdateDto, UserService, UserRepository>() {
private val passwordEncoder = BCryptPasswordEncoder()
override val entity: User = user(id = 0L, passwordEncoder = passwordEncoder)
override val anotherEntity: User = user(id = 1L, passwordEncoder = passwordEncoder)
private val entityDefaultGroupUser = user(id = 2L, isDefaultGroupUser = true, passwordEncoder = passwordEncoder)
private val entitySystemUser = user(id = 3L, isSystemUser = true, passwordEncoder = passwordEncoder)
override val entity: User = user(passwordEncoder, id = 0L)
override val anotherEntity: User = user(passwordEncoder, id = 1L)
private val entityDefaultGroupUser = user(passwordEncoder, id = 2L, isDefaultGroupUser = true)
private val entitySystemUser = user(passwordEncoder, id = 3L, isSystemUser = true)
private val group = group(id = 0L)
override val entitySaveDto: UserSaveDto = spy(userSaveDto(passwordEncoder, id = 0L))
override val entityUpdateDto: UserUpdateDto = spy(userUpdateDto(id = 0L))
override val repository: UserRepository = mock()
private val groupService: GroupService = mock()
override val service: UserService = spy(UserServiceImpl(repository, groupService))
override val service: UserService = spy(UserServiceImpl(repository, groupService, passwordEncoder))
private val entitySaveDtoUser = User(
entitySaveDto.id,
@ -210,7 +210,7 @@ class GroupServiceTest :
override val entityWithEntityName: Group = group(id = 2L, name = entity.name)
private val groupUserId = 1000000L + entity.id!!
private val groupUser = user(passwordEncoder = BCryptPasswordEncoder(), id = groupUserId, group = entity)
private val groupUser = user(BCryptPasswordEncoder(), id = groupUserId, group = entity)
@BeforeEach
override fun afterEach() {
@ -303,7 +303,7 @@ class GroupServiceTest :
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserUserDetailsServiceTest {
private val userService: UserService = mock()
private val service = spy(UserDetailsServiceImpl(userService))
private val service = spy(CreUserDetailsServiceImpl(userService))
private val user = user(id = 0L)
@ -317,8 +317,8 @@ class UserUserDetailsServiceTest {
@Test
fun `loadUserByUsername() calls loadUserByUserId() with the given username as an id`() {
whenever(userService.getById(eq(user.id), any(), any())).doReturn(user)
doReturn(UserDetails(user(id = user.id, plainPassword = user.password)))
.whenever(service).loadUserById(user.id)
doReturn(SpringUser(user.id.toString(), user.password, listOf())).whenever(service)
.loadUserById(user.id)
service.loadUserByUsername(user.id.toString())

View File

@ -6,7 +6,6 @@ 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