From 64349b25e9161d1576fd5104539f4cf760da9ec0 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Thu, 28 Apr 2022 21:29:28 -0400 Subject: [PATCH] #30 Increase JWT security by removing useless information, return useful information in the login response body instead. Remove default group users from code base. --- .../GroupTokenAuthenticationProvider.kt | 3 +- .../config/security/SecurityConfig.kt | 13 ++-- .../filters/GroupTokenAuthenticationFilter.kt | 2 +- .../filters/JwtAuthenticationFilter.kt | 29 +++++++-- .../filters/JwtAuthorizationFilter.kt | 65 +++++++------------ .../UsernamePasswordAuthenticationFilter.kt | 3 +- .../dtos/account/GroupDto.kt | 9 +-- .../dtos/account/UserDto.kt | 51 +++++++-------- .../logic/account/GroupLogic.kt | 40 +----------- .../logic/account/JwtLogic.kt | 37 +++++++---- .../logic/account/UserDetailsLogic.kt | 6 +- .../logic/account/UserLogic.kt | 38 ++--------- .../repository/AccountRepository.kt | 20 ++++-- .../rest/account/GroupController.kt | 23 +------ .../rest/account/GroupTokenController.kt | 2 + .../service/account/UserService.kt | 35 ++++------ src/main/resources/application.properties | 4 +- .../logic/account/DefaultGroupLogicTest.kt | 1 - 18 files changed, 139 insertions(+), 242 deletions(-) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/GroupTokenAuthenticationProvider.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/GroupTokenAuthenticationProvider.kt index 8150f07..2001f35 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/GroupTokenAuthenticationProvider.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/GroupTokenAuthenticationProvider.kt @@ -16,7 +16,8 @@ class GroupTokenAuthenticationProvider(private val groupTokenLogic: GroupTokenLo val groupTokenId = parseGroupTokenId(groupAuthenticationToken.id) val groupToken = retrieveGroupToken(groupTokenId) - val userDetails = UserDetails(groupToken.id, groupToken.name, "", groupToken.group.id, groupToken.group.permissions) + val userDetails = + UserDetails(groupToken.id.toString(), groupToken.name, "", groupToken.group, groupToken.group.permissions) return UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/SecurityConfig.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/SecurityConfig.kt index 34485d3..de1d0fe 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/SecurityConfig.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/SecurityConfig.kt @@ -3,8 +3,8 @@ package dev.fyloz.colorrecipesexplorer.config.security import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties import dev.fyloz.colorrecipesexplorer.config.security.filters.GroupTokenAuthenticationFilter -import dev.fyloz.colorrecipesexplorer.config.security.filters.UsernamePasswordAuthenticationFilter import dev.fyloz.colorrecipesexplorer.config.security.filters.JwtAuthorizationFilter +import dev.fyloz.colorrecipesexplorer.config.security.filters.UsernamePasswordAuthenticationFilter import dev.fyloz.colorrecipesexplorer.dtos.account.UserDto import dev.fyloz.colorrecipesexplorer.emergencyMode import dev.fyloz.colorrecipesexplorer.logic.account.GroupTokenLogic @@ -19,7 +19,6 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Profile -import org.springframework.core.annotation.Order import org.springframework.core.env.Environment import org.springframework.http.HttpMethod import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder @@ -102,15 +101,14 @@ abstract class BaseSecurityConfig( BasicAuthenticationFilter::class.java ) .addFilter( - JwtAuthorizationFilter(jwtLogic, authenticationManager(), userDetailsLogic) + JwtAuthorizationFilter(jwtLogic, authenticationManager()) ) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() -// .antMatchers("/api/config/**").permitAll() // Allow access to logo and icon -// .antMatchers("/api/account/login/group").permitAll() // Allow access to login -// .antMatchers("**").fullyAuthenticated() - .antMatchers("**").permitAll() + .antMatchers("/api/config/**").permitAll() // Allow access to logo and icon + .antMatchers("/api/account/login/**").permitAll() // Allow access to login + .antMatchers("**").fullyAuthenticated() if (Constants.DEBUG_MODE) { http @@ -130,7 +128,6 @@ abstract class BaseSecurityConfig( } } -@Order(2) @Configuration @Profile("!emergency") @EnableWebSecurity diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/GroupTokenAuthenticationFilter.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/GroupTokenAuthenticationFilter.kt index 685bf8a..ede8678 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/GroupTokenAuthenticationFilter.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/GroupTokenAuthenticationFilter.kt @@ -27,7 +27,7 @@ class GroupTokenAuthenticationFilter( } override fun afterSuccessfulAuthentication(userDetails: UserDetails) { - logger.info("Successful login for group id '${userDetails.groupId}' using token '${userDetails.id}' (${userDetails.username})") + logger.info("Successful login for group id '${userDetails.group!!.id}' using token '${userDetails.id}' (${userDetails.username})") } private fun getGroupTokenCookie(request: HttpServletRequest) = diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/JwtAuthenticationFilter.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/JwtAuthenticationFilter.kt index 95136eb..02b9aa9 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/JwtAuthenticationFilter.kt @@ -1,8 +1,10 @@ package dev.fyloz.colorrecipesexplorer.config.security.filters +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties import dev.fyloz.colorrecipesexplorer.dtos.account.UserDetails +import dev.fyloz.colorrecipesexplorer.dtos.account.UserLoginResponse import dev.fyloz.colorrecipesexplorer.logic.account.JwtLogic import dev.fyloz.colorrecipesexplorer.utils.addCookie import org.springframework.http.HttpMethod @@ -21,6 +23,8 @@ abstract class JwtAuthenticationFilter( AbstractAuthenticationProcessingFilter( AntPathRequestMatcher(filterProcessesUrl, HttpMethod.POST.toString()) ) { + private val jacksonObjectMapper = jacksonObjectMapper() + override fun successfulAuthentication( request: HttpServletRequest, response: HttpServletResponse, @@ -30,19 +34,14 @@ abstract class JwtAuthenticationFilter( val userDetails = auth.principal as UserDetails val token = jwtLogic.buildJwt(userDetails) - addAuthorizationHeaders(response, token) addAuthorizationCookie(response, token) + addResponseBody(userDetails, response) afterSuccessfulAuthentication(userDetails) } protected abstract fun afterSuccessfulAuthentication(userDetails: UserDetails) - private fun addAuthorizationHeaders(response: HttpServletResponse, token: String) { - response.addHeader(Constants.HeaderNames.ACCESS_CONTROL_EXPOSE_HEADERS, Constants.HeaderNames.AUTHORIZATION) - response.addHeader(Constants.HeaderNames.AUTHORIZATION, "$BEARER_TOKEN_PREFIX $token") - } - private fun addAuthorizationCookie(response: HttpServletResponse, token: String) { response.addCookie(Constants.CookieNames.AUTHORIZATION, BEARER_TOKEN_PREFIX + token) { httpOnly = AUTHORIZATION_COOKIE_HTTP_ONLY @@ -53,11 +52,27 @@ abstract class JwtAuthenticationFilter( } } + private fun addResponseBody(userDetails: UserDetails, response: HttpServletResponse) { + val body = getResponseBody(userDetails) + val serializedBody = jacksonObjectMapper.writeValueAsString(body) + + response.writer.println(serializedBody) + } + + private fun getResponseBody(userDetails: UserDetails) = + UserLoginResponse( + userDetails.id, + userDetails.username, + userDetails.group?.id, + userDetails.group?.name, + userDetails.permissions + ) + companion object { private const val AUTHORIZATION_COOKIE_HTTP_ONLY = true private const val AUTHORIZATION_COOKIE_SAME_SITE = true private const val AUTHORIZATION_COOKIE_PATH = Constants.ControllerPaths.BASE_PATH - private const val BEARER_TOKEN_PREFIX = "Bearer" + const val BEARER_TOKEN_PREFIX = "Bearer" } } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/JwtAuthorizationFilter.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/JwtAuthorizationFilter.kt index 8d1a7f5..d7f8cb5 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/JwtAuthorizationFilter.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/JwtAuthorizationFilter.kt @@ -1,13 +1,12 @@ package dev.fyloz.colorrecipesexplorer.config.security.filters import dev.fyloz.colorrecipesexplorer.Constants -import dev.fyloz.colorrecipesexplorer.dtos.account.UserDetails -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.logic.account.JwtLogic -import dev.fyloz.colorrecipesexplorer.logic.account.UserDetailsLogic +import dev.fyloz.colorrecipesexplorer.logic.account.UserJwt import io.jsonwebtoken.ExpiredJwtException 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.web.authentication.www.BasicAuthenticationFilter import org.springframework.web.util.WebUtils @@ -17,60 +16,42 @@ import javax.servlet.http.HttpServletResponse class JwtAuthorizationFilter( private val jwtLogic: JwtLogic, - authenticationManager: AuthenticationManager, - private val userDetailsLogic: UserDetailsLogic + authenticationManager: AuthenticationManager ) : BasicAuthenticationFilter(authenticationManager) { override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) { - fun tryLoginFromBearer(): Boolean { - val authorizationCookie = WebUtils.getCookie(request, Constants.HeaderNames.AUTHORIZATION) - // Check for an authorization token cookie or header - val authorizationToken = if (authorizationCookie != null) - authorizationCookie.value - else - request.getHeader(Constants.HeaderNames.AUTHORIZATION) + val authorizationCookie = WebUtils.getCookie(request, Constants.HeaderNames.AUTHORIZATION) - // An authorization token is valid if it starts with "Bearer", is not expired and is not blacklisted - if (authorizationToken != null && authorizationToken.startsWith("Bearer") && authorizationToken !in blacklistedJwtTokens) { - val authenticationToken = getAuthentication(authorizationToken) ?: return false - SecurityContextHolder.getContext().authentication = authenticationToken - return true - } - return false + // If there is no authorization cookie, the user is not authenticated + if (authorizationCookie == null) { + chain.doFilter(request, response) + return } - fun tryLoginFromDefaultGroupCookie() { - val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName) - if (defaultGroupCookie != null) { - val authenticationToken = getAuthenticationToken(defaultGroupCookie.value) - SecurityContextHolder.getContext().authentication = authenticationToken - } + val authorizationToken = authorizationCookie.value + if (!isJwtValid(authorizationToken)) { + chain.doFilter(request, response) + return } - if (!tryLoginFromBearer()) - tryLoginFromDefaultGroupCookie() - + SecurityContextHolder.getContext().authentication = getAuthentication(authorizationToken) chain.doFilter(request, response) } - private fun getAuthentication(token: String): UsernamePasswordAuthenticationToken? { + // The authorization token is valid if it starts with "Bearer" + private fun isJwtValid(authorizationToken: String) = + authorizationToken.startsWith(JwtAuthenticationFilter.BEARER_TOKEN_PREFIX) + + private fun getAuthentication(authorizationToken: String): Authentication? { return try { - val user = jwtLogic.parseJwt(token.replace("Bearer", "")) - getAuthenticationToken(user) + val jwt = authorizationToken.replace(JwtAuthenticationFilter.BEARER_TOKEN_PREFIX, "").trim() + val user = jwtLogic.parseJwt(jwt) + + getAuthentication(user) } catch (_: ExpiredJwtException) { null } } - private fun getAuthenticationToken(user: UserDetails) = + private fun getAuthentication(user: UserJwt) = UsernamePasswordAuthenticationToken(user.id, null, user.authorities) - - private fun getAuthenticationToken(userId: Long): UsernamePasswordAuthenticationToken? = try { - val userDetails = userDetailsLogic.loadUserById(userId) - UsernamePasswordAuthenticationToken(userDetails.username, null, userDetails.authorities) - } catch (_: NotFoundException) { - null - } - - private fun getAuthenticationToken(userId: String) = - getAuthenticationToken(userId.toLong()) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/UsernamePasswordAuthenticationFilter.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/UsernamePasswordAuthenticationFilter.kt index d9d91ec..cc44d14 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/UsernamePasswordAuthenticationFilter.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/filters/UsernamePasswordAuthenticationFilter.kt @@ -12,7 +12,6 @@ import org.springframework.security.core.Authentication import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse -const val defaultGroupCookieName = "Default-Group" val blacklistedJwtTokens = mutableListOf() class UsernamePasswordAuthenticationFilter( @@ -30,7 +29,7 @@ class UsernamePasswordAuthenticationFilter( } override fun afterSuccessfulAuthentication(userDetails: UserDetails) { - updateUserLoginTime(userDetails.id as Long) + updateUserLoginTime(userDetails.id.toLong()) logger.info("User ${userDetails.id} (${userDetails.username}) has logged in successfully") } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/account/GroupDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/account/GroupDto.kt index 8e5a61c..c756779 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/account/GroupDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/account/GroupDto.kt @@ -16,11 +16,4 @@ data class GroupDto( val permissions: List, val explicitPermissions: List = listOf() -) : EntityDto { - @get:JsonIgnore - val defaultGroupUserId = getDefaultGroupUserId(id) - - companion object { - fun getDefaultGroupUserId(id: Long) = 1000000 + id - } -} +) : EntityDto diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/account/UserDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/account/UserDto.kt index 525a333..1dbb98a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/account/UserDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/account/UserDto.kt @@ -17,8 +17,7 @@ data class UserDto( val lastName: String, - @field:JsonIgnore - val password: String = "", + @field:JsonIgnore val password: String = "", val group: GroupDto?, @@ -28,11 +27,7 @@ data class UserDto( val lastLoginTime: LocalDateTime? = null, - @field:JsonIgnore - val isDefaultGroupUser: Boolean = false, - - @field:JsonIgnore - val isSystemUser: Boolean = false + @field:JsonIgnore val isSystemUser: Boolean = false ) : EntityDto { @get:JsonIgnore val fullName = "$firstName $lastName" @@ -41,35 +36,29 @@ data class UserDto( data class UserSaveDto( val id: Long = 0L, - @field:NotBlank - val firstName: String, + @field:NotBlank val firstName: String, - @field:NotBlank - val lastName: String, + @field:NotBlank val lastName: String, - @field:NotBlank - @field:Size(min = 8, message = Constants.ValidationMessages.PASSWORD_TOO_SMALL) - val password: String, + @field:NotBlank @field:Size( + min = 8, message = Constants.ValidationMessages.PASSWORD_TOO_SMALL + ) val password: String, val groupId: Long?, val permissions: List, - @field:JsonIgnore - val isSystemUser: Boolean = false, + @field:JsonIgnore val isSystemUser: Boolean = false, - @field:JsonIgnore - val isDefaultGroupUser: Boolean = false + @field:JsonIgnore val isDefaultGroupUser: Boolean = false ) data class UserUpdateDto( val id: Long = 0L, - @field:NotBlank - val firstName: String, + @field:NotBlank val firstName: String, - @field:NotBlank - val lastName: String, + @field:NotBlank val lastName: String, val groupId: Long?, @@ -79,24 +68,30 @@ data class UserUpdateDto( data class UserLoginRequestDto(val id: Long, val password: String) class UserDetails( - val id: Any, + val id: String, private val username: String, private val password: String, - val groupId: Long?, + val group: GroupDto?, val permissions: Collection ) : SpringUserDetails { - constructor(user: UserDto) : this(user.id, user.fullName, user.password, user.group?.id, user.permissions) + constructor(user: UserDto) : this(user.id.toString(), user.fullName, user.password, user.group, user.permissions) override fun getUsername() = username override fun getPassword() = password @JsonIgnore - override fun getAuthorities() = permissions - .map { it.toAuthority() } - .toMutableList() + override fun getAuthorities() = permissions.map { it.toAuthority() }.toMutableList() override fun isAccountNonExpired() = true override fun isAccountNonLocked() = true override fun isCredentialsNonExpired() = true override fun isEnabled() = true } + +data class UserLoginResponse( + val id: String, + val fullName: String, + val groupId: Long?, + val groupName: String?, + val permissions: Collection +) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/GroupLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/GroupLogic.kt index 8b60e77..8780ddc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/GroupLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/GroupLogic.kt @@ -4,26 +4,14 @@ import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.dtos.account.GroupDto import dev.fyloz.colorrecipesexplorer.dtos.account.UserDto -import dev.fyloz.colorrecipesexplorer.exception.NoDefaultGroupException import dev.fyloz.colorrecipesexplorer.logic.BaseLogic import dev.fyloz.colorrecipesexplorer.logic.Logic import dev.fyloz.colorrecipesexplorer.service.account.GroupService import org.springframework.transaction.annotation.Transactional -import org.springframework.web.util.WebUtils -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse - -const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans interface GroupLogic : Logic { /** Gets all the users of the group with the given [id]. */ fun getUsersForGroup(id: Long): Collection - - /** Gets the default group from a cookie in the given HTTP [request]. */ - fun getRequestDefaultGroup(request: HttpServletRequest): GroupDto - - /** Sets the default group cookie for the given HTTP [response]. */ - fun setResponseDefaultGroup(id: Long, response: HttpServletResponse) } @LogicComponent @@ -32,32 +20,11 @@ class DefaultGroupLogic(service: GroupService, private val userLogic: UserLogic) GroupLogic { override fun getUsersForGroup(id: Long) = userLogic.getAllByGroup(getById(id)) - override fun getRequestDefaultGroup(request: HttpServletRequest): GroupDto { - val defaultGroupCookie = WebUtils.getCookie(request, Constants.CookieNames.GROUP_TOKEN) - ?: throw NoDefaultGroupException() - val defaultGroupUser = userLogic.getById( - defaultGroupCookie.value.toLong(), - isSystemUser = false, - isDefaultGroupUser = true - ) - return defaultGroupUser.group!! - } - - override fun setResponseDefaultGroup(id: Long, response: HttpServletResponse) { - val defaultGroupUser = userLogic.getDefaultGroupUser(getById(id)) - response.addHeader( - "Set-Cookie", - "${Constants.CookieNames.GROUP_TOKEN}=${defaultGroupUser.id}; Max-Age=$defaultGroupCookieMaxAge; Path=/api; HttpOnly; Secure; SameSite=strict" - ) - } - @Transactional override fun save(dto: GroupDto): GroupDto { throwIfNameAlreadyExists(dto.name) - return super.save(dto).also { - userLogic.saveDefaultGroupUser(it) - } + return super.save(dto) } override fun update(dto: GroupDto): GroupDto { @@ -66,11 +33,6 @@ class DefaultGroupLogic(service: GroupService, private val userLogic: UserLogic) return super.update(dto) } - override fun deleteById(id: Long) { - userLogic.deleteById(GroupDto.getDefaultGroupUserId(id)) - super.deleteById(id) - } - private fun throwIfNameAlreadyExists(name: String, id: Long? = null) { if (service.existsByName(name, id)) { throw alreadyExistsException(value = name) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/JwtLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/JwtLogic.kt index 3897561..5938e98 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/JwtLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/JwtLogic.kt @@ -4,11 +4,14 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties import dev.fyloz.colorrecipesexplorer.dtos.account.UserDetails +import dev.fyloz.colorrecipesexplorer.model.account.Permission +import dev.fyloz.colorrecipesexplorer.model.account.toAuthority import dev.fyloz.colorrecipesexplorer.utils.base64encode import dev.fyloz.colorrecipesexplorer.utils.toDate import io.jsonwebtoken.Jwts import io.jsonwebtoken.jackson.io.JacksonDeserializer import io.jsonwebtoken.jackson.io.JacksonSerializer +import org.springframework.security.core.GrantedAuthority import org.springframework.stereotype.Service import java.time.Instant import java.util.* @@ -19,8 +22,8 @@ interface JwtLogic { /** Build a JWT token for the given [userDetails]. */ fun buildJwt(userDetails: UserDetails): String - /** Parses a user from the given [jwt] token. */ - fun parseJwt(jwt: String): UserDetails + /** Parses a user information from the given [jwt] token. */ + fun parseJwt(jwt: String): UserJwt } @Service @@ -47,24 +50,32 @@ class DefaultJwtLogic( override fun buildJwt(userDetails: UserDetails): String = jwtBuilder - .setSubject(userDetails.id.toString()) + .setSubject(userDetails.id) .setExpiration(getCurrentExpirationDate()) - .claim(jwtClaimUser, userDetails.serialize()) + .claim(JWT_CLAIM_PERMISSIONS, objectMapper.writeValueAsString(userDetails.permissions)) .compact() - override fun parseJwt(jwt: String): UserDetails = - with( - jwtParser.parseClaimsJws(jwt) - .body.get(jwtClaimUser, String::class.java) - ) { - objectMapper.readValue(this) - } + override fun parseJwt(jwt: String): UserJwt { + val parsedJwt = jwtParser.parseClaimsJws(jwt) + + val serializedPermissions = parsedJwt.body.get(JWT_CLAIM_PERMISSIONS, String::class.java) + val permissions = objectMapper.readValue>(serializedPermissions) + + val authorities = permissions + .map { it.toAuthority() } + .toMutableList() + + return UserJwt(parsedJwt.body.subject, authorities) + } private fun getCurrentExpirationDate(): Date = Instant.now() .plusSeconds(securityProperties.jwtDuration) .toDate() - private fun UserDetails.serialize(): String = - objectMapper.writeValueAsString(this) + companion object { + private const val JWT_CLAIM_PERMISSIONS = "permissions" + } } + +data class UserJwt(val id: String, val authorities: Collection) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/UserDetailsLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/UserDetailsLogic.kt index 8a621ab..972dee1 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/UserDetailsLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/UserDetailsLogic.kt @@ -32,11 +32,7 @@ class DefaultUserDetailsLogic( } override fun loadUserById(id: Long, isDefaultGroupUser: Boolean): UserDetails { - val user = userLogic.getById( - id, - isSystemUser = true, - isDefaultGroupUser = isDefaultGroupUser - ) + val user = userLogic.getById(id, isSystemUser = true) return UserDetails(user) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/UserLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/UserLogic.kt index 07046bc..6ca5ad6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/UserLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/UserLogic.kt @@ -23,13 +23,7 @@ interface UserLogic : Logic { fun getAllByGroup(group: GroupDto): Collection /** Gets the user with the given [id]. */ - fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean): UserDto - - /** Gets the default user of the given [group]. */ - fun getDefaultGroupUser(group: GroupDto): UserDto - - /** Save a default group user for the given [group]. */ - fun saveDefaultGroupUser(group: GroupDto) + fun getById(id: Long, isSystemUser: Boolean): UserDto /** Saves the given [dto]. */ fun save(dto: UserSaveDto): UserDto @@ -57,30 +51,13 @@ interface UserLogic : Logic { class DefaultUserLogic( service: UserService, @Lazy private val groupLogic: GroupLogic, @Lazy private val passwordEncoder: PasswordEncoder ) : BaseLogic(service, Constants.ModelNames.USER), UserLogic { - override fun getAll() = service.getAll(isSystemUser = false, isDefaultGroupUser = false) + override fun getAll() = service.getAll(false) override fun getAllByGroup(group: GroupDto) = service.getAllByGroup(group) - override fun getById(id: Long) = getById(id, isSystemUser = false, isDefaultGroupUser = false) - override fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean) = - service.getById(id, !isDefaultGroupUser, !isSystemUser) ?: throw notFoundException(value = id) - - override fun getDefaultGroupUser(group: GroupDto) = - service.getDefaultGroupUser(group) ?: throw notFoundException(identifierName = "groupId", value = group.id) - - override fun saveDefaultGroupUser(group: GroupDto) { - save( - UserSaveDto( - id = group.defaultGroupUserId, - firstName = group.name, - lastName = "User", - password = group.name, - groupId = group.id, - permissions = listOf(), - isDefaultGroupUser = true - ) - ) - } + override fun getById(id: Long) = getById(id, false) + override fun getById(id: Long, isSystemUser: Boolean) = + service.getById(id, !isSystemUser) ?: throw notFoundException(value = id) override fun save(dto: UserSaveDto) = save( UserDto( @@ -90,8 +67,7 @@ class DefaultUserLogic( password = passwordEncoder.encode(dto.password), group = if (dto.groupId != null) groupLogic.getById(dto.groupId) else null, permissions = dto.permissions, - isSystemUser = dto.isSystemUser, - isDefaultGroupUser = dto.isDefaultGroupUser + isSystemUser = dto.isSystemUser ) ) @@ -103,7 +79,7 @@ class DefaultUserLogic( } override fun update(dto: UserUpdateDto): UserDto { - val user = getById(dto.id, isSystemUser = false, isDefaultGroupUser = false) + val user = getById(dto.id, isSystemUser = false) return update( user.copy( diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/AccountRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/AccountRepository.kt index 31e8d9f..e530d45 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/AccountRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/AccountRepository.kt @@ -1,28 +1,34 @@ package dev.fyloz.colorrecipesexplorer.repository -import dev.fyloz.colorrecipesexplorer.model.account.GroupToken import dev.fyloz.colorrecipesexplorer.model.account.Group +import dev.fyloz.colorrecipesexplorer.model.account.GroupToken import dev.fyloz.colorrecipesexplorer.model.account.User import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository -import java.util.UUID +import java.util.* +/** + * Default group users are deprecated and should not be used anymore. + * To prevent data loss, they will not be removed from the database, + * but they are excluded from results from the database. + */ @Repository interface UserRepository : JpaRepository { + fun findAllByIsDefaultGroupUserIsFalse(): MutableList + + fun findByIdAndIsDefaultGroupUserIsFalse(id: Long): User? + /** Checks if a user with the given [firstName], [lastName] and a different [id] exists. */ - fun existsByFirstNameAndLastNameAndIdNot(firstName: String, lastName: String, id: Long): Boolean + fun existsByFirstNameAndLastNameAndIdNotAndIsDefaultGroupUserIsFalse(firstName: String, lastName: String, id: Long): Boolean /** Finds all users for the given [group]. */ @Query("SELECT u FROM User u WHERE u.group = :group AND u.isSystemUser IS FALSE AND u.isDefaultGroupUser IS FALSE") fun findAllByGroup(group: Group): Collection /** Finds the user with the given [firstName] and [lastName]. */ + @Query("SELECT u From User u WHERE u.firstName = :firstName AND u.lastName = :lastName") fun findByFirstNameAndLastName(firstName: String, lastName: String): User? - - /** Finds the default user for the given [group]. */ - @Query("SELECT u FROM User u WHERE u.group = :group AND u.isDefaultGroupUser IS TRUE") - fun findDefaultGroupUser(group: Group): User? } @Repository diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/account/GroupController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/account/GroupController.kt index ac3971c..3626696 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/account/GroupController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/account/GroupController.kt @@ -21,8 +21,7 @@ import javax.validation.Valid @RequestMapping(Constants.ControllerPaths.GROUP) @Profile("!emergency") class GroupController( - private val groupLogic: GroupLogic, - private val userLogic: UserLogic + private val groupLogic: GroupLogic ) { @GetMapping @PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')") @@ -39,26 +38,6 @@ class GroupController( fun getUsersForGroup(@PathVariable id: Long) = ok(groupLogic.getUsersForGroup(id)) - @PostMapping("default/{groupId}") - @PreAuthorizeViewUsers - fun setDefaultGroup(@PathVariable groupId: Long, response: HttpServletResponse) = - noContent { - groupLogic.setResponseDefaultGroup(groupId, response) - } - - @GetMapping("default") - @PreAuthorizeViewUsers - fun getRequestDefaultGroup(request: HttpServletRequest) = - ok(with(groupLogic) { - getRequestDefaultGroup(request) - }) - - @GetMapping("currentuser") - fun getCurrentGroupUser(request: HttpServletRequest) = - ok(with(groupLogic.getRequestDefaultGroup(request)) { - userLogic.getDefaultGroupUser(this) - }) - @PostMapping @PreAuthorizeEditUsers fun save(@Valid @RequestBody group: GroupDto) = diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/account/GroupTokenController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/account/GroupTokenController.kt index 79170eb..75e69f3 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/account/GroupTokenController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/account/GroupTokenController.kt @@ -25,6 +25,8 @@ class GroupTokenController(private val groupTokenLogic: GroupTokenLogic) { @GetMapping("{id}") fun getById(@PathVariable id: String) = ok(groupTokenLogic.getById(id)) + // TODO Remove when group tokens will be fully implemented + @Deprecated("Only use for testing purposes") @GetMapping("{id}/cookie") fun addCookieForId(@PathVariable id: String, response: HttpServletResponse) { val groupToken = groupTokenLogic.getById(id) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/account/UserService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/account/UserService.kt index d942ab7..31c5a23 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/account/UserService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/account/UserService.kt @@ -9,49 +9,42 @@ import dev.fyloz.colorrecipesexplorer.model.account.flat import dev.fyloz.colorrecipesexplorer.repository.UserRepository import dev.fyloz.colorrecipesexplorer.service.BaseService import dev.fyloz.colorrecipesexplorer.service.Service -import org.springframework.data.repository.findByIdOrNull interface UserService : Service { /** Checks if a user with the given [firstName] and [lastName] exists. */ fun existsByFirstNameAndLastName(firstName: String, lastName: String, id: Long? = null): Boolean - /** Gets all users, depending on [isSystemUser] and [isDefaultGroupUser]. */ - fun getAll(isSystemUser: Boolean, isDefaultGroupUser: Boolean): Collection + /** Gets all users, depending on [isSystemUser]. */ + fun getAll(isSystemUser: Boolean): Collection /** Gets all users for the given [group]. */ fun getAllByGroup(group: GroupDto): Collection - /** Finds the user with the given [id], depending on [isSystemUser] and [isDefaultGroupUser]. */ - fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean): UserDto? + /** Finds the user with the given [id], depending on [isSystemUser]. */ + fun getById(id: Long, isSystemUser: Boolean): UserDto? /** Finds the user with the given [firstName] and [lastName]. */ fun getByFirstNameAndLastName(firstName: String, lastName: String): UserDto? - - /** Find the default user for the given [group]. */ - fun getDefaultGroupUser(group: GroupDto): UserDto? } @ServiceComponent class DefaultUserService(repository: UserRepository, private val groupService: GroupService) : BaseService(repository), UserService { override fun existsByFirstNameAndLastName(firstName: String, lastName: String, id: Long?) = - repository.existsByFirstNameAndLastNameAndIdNot(firstName, lastName, id ?: 0L) + repository.existsByFirstNameAndLastNameAndIdNotAndIsDefaultGroupUserIsFalse(firstName, lastName, id ?: 0L) - override fun getAll(isSystemUser: Boolean, isDefaultGroupUser: Boolean) = - repository.findAll() + override fun getAll(isSystemUser: Boolean) = + repository.findAllByIsDefaultGroupUserIsFalse() .filter { isSystemUser || !it.isSystemUser } - .filter { isDefaultGroupUser || !it.isDefaultGroupUser } .map(::toDto) override fun getAllByGroup(group: GroupDto) = repository.findAllByGroup(groupService.toEntity(group)) .map(::toDto) - override fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean): UserDto? { - val user = repository.findByIdOrNull(id) ?: return null - if ((!isSystemUser && user.isSystemUser) || - !isDefaultGroupUser && user.isDefaultGroupUser - ) { + override fun getById(id: Long, isSystemUser: Boolean): UserDto? { + val user = repository.findByIdAndIsDefaultGroupUserIsFalse(id) ?: return null + if (!isSystemUser && user.isSystemUser) { return null } @@ -63,11 +56,6 @@ class DefaultUserService(repository: UserRepository, private val groupService: G return if (user != null) toDto(user) else null } - override fun getDefaultGroupUser(group: GroupDto): UserDto? { - val user = repository.findDefaultGroupUser(groupService.toEntity(group)) - return if (user != null) toDto(user) else null - } - override fun toDto(entity: User) = UserDto( entity.id, entity.firstName, @@ -77,7 +65,6 @@ class DefaultUserService(repository: UserRepository, private val groupService: G getFlattenPermissions(entity), entity.permissions, entity.lastLoginTime, - entity.isDefaultGroupUser, entity.isSystemUser ) @@ -86,7 +73,7 @@ class DefaultUserService(repository: UserRepository, private val groupService: G dto.firstName, dto.lastName, dto.password, - dto.isDefaultGroupUser, + false, dto.isSystemUser, if (dto.group != null) groupService.toEntity(dto.group) else null, dto.explicitPermissions, diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a899897..8a271b9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -30,6 +30,4 @@ spring.jackson.deserialization.fail-on-null-for-primitives=true spring.jackson.default-property-inclusion=non_null spring.profiles.active=@spring.profiles.active@ -spring.sql.init.continue-on-error=true - -spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration \ No newline at end of file +spring.sql.init.continue-on-error=true \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/DefaultGroupLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/DefaultGroupLogicTest.kt index fa5d8bf..2c9ea17 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/DefaultGroupLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/DefaultGroupLogicTest.kt @@ -25,7 +25,6 @@ class DefaultGroupLogicTest { every { getAllByGroup(any()) } returns listOf() every { getById(any(), any(), any()) } returns user every { getDefaultGroupUser(any()) } returns user - every { saveDefaultGroupUser(any()) } just runs every { deleteById(any()) } just runs }