diff --git a/pom.xml b/pom.xml index d592f91..6bf7629 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,7 @@ ch.qos.logback logback-classic - 1.4.7 + 1.4.12 org.slf4j diff --git a/webapp/person.http b/webapp/person.http index 33be8b7..3ee3f1e 100644 --- a/webapp/person.http +++ b/webapp/person.http @@ -10,13 +10,15 @@ Content-Type: application/json } ### User login +< {% + const pwd = request.environment.get("testUserPwd") + const userPwd = 'me@koldyr.com:' + pwd; + const basicAuthValue = crypto.md5().updateWithText(userPwd).digest().toBase64() + request.variables.set("basicAuthValue", basicAuthValue) +%} POST http://{{server}}/api/v1/user/login -Content-Type: application/json - -{ - "username": "me@koldyr.com", - "password": "{{testUserPwd}}" -} +Accept: application/json +Authorization: Basic {{basicAuthValue}} ### Get All Persons GET http://{{server}}/api/v1/lineage/1/persons diff --git a/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/UserController.kt b/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/UserController.kt index 015e476..3722c2a 100644 --- a/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/UserController.kt +++ b/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/UserController.kt @@ -4,15 +4,16 @@ import java.net.URI import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.tags.Tags import jakarta.validation.Valid +import jakarta.validation.constraints.Size import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity.created import org.springframework.http.ResponseEntity.ok import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -import com.koldyr.genealogy.dto.Credentials import com.koldyr.genealogy.dto.UserDTO import com.koldyr.genealogy.model.User import com.koldyr.genealogy.services.UserService @@ -33,7 +34,7 @@ class UserController( } @PostMapping("/login") - fun login(@RequestBody @Valid credentials: Credentials): ResponseEntity { + fun login(@RequestHeader(AUTHORIZATION) @Size(max = 256) credentials: String): ResponseEntity { val user = userService.login(credentials) return ok() diff --git a/webapp/src/main/kotlin/com/koldyr/genealogy/services/UserService.kt b/webapp/src/main/kotlin/com/koldyr/genealogy/services/UserService.kt index f3128f0..f0ad525 100644 --- a/webapp/src/main/kotlin/com/koldyr/genealogy/services/UserService.kt +++ b/webapp/src/main/kotlin/com/koldyr/genealogy/services/UserService.kt @@ -1,6 +1,5 @@ package com.koldyr.genealogy.services -import com.koldyr.genealogy.dto.Credentials import com.koldyr.genealogy.dto.LineageUser import com.koldyr.genealogy.model.User @@ -13,5 +12,5 @@ import com.koldyr.genealogy.model.User interface UserService { fun create(user: User) fun currentUser(): User - fun login(credentials: Credentials): LineageUser + fun login(credentials: String): LineageUser } diff --git a/webapp/src/main/kotlin/com/koldyr/genealogy/services/UserServiceImpl.kt b/webapp/src/main/kotlin/com/koldyr/genealogy/services/UserServiceImpl.kt index b437687..d3b8141 100644 --- a/webapp/src/main/kotlin/com/koldyr/genealogy/services/UserServiceImpl.kt +++ b/webapp/src/main/kotlin/com/koldyr/genealogy/services/UserServiceImpl.kt @@ -3,9 +3,17 @@ package com.koldyr.genealogy.services import java.time.LocalDateTime import java.time.ZoneId import java.util.* +import kotlin.text.Charsets.UTF_8 +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.JWSHeader +import com.nimbusds.jose.crypto.MACSigner +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.SignedJWT import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value -import org.springframework.http.HttpStatus.* +import org.springframework.http.HttpStatus.BAD_REQUEST +import org.springframework.http.HttpStatus.FORBIDDEN +import org.springframework.http.HttpStatus.NOT_FOUND import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.UsernamePasswordAuthenticationToken.unauthenticated import org.springframework.security.core.Authentication @@ -15,12 +23,6 @@ import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import org.springframework.web.server.ResponseStatusException -import com.nimbusds.jose.JWSAlgorithm -import com.nimbusds.jose.JWSHeader -import com.nimbusds.jose.crypto.MACSigner -import com.nimbusds.jwt.JWTClaimsSet -import com.nimbusds.jwt.SignedJWT -import com.koldyr.genealogy.dto.Credentials import com.koldyr.genealogy.dto.LineageUser import com.koldyr.genealogy.model.User import com.koldyr.genealogy.persistence.RoleRepository @@ -71,20 +73,34 @@ class UserServiceImpl( } @Transactional(readOnly = true) - override fun login(credentials: Credentials): LineageUser { + override fun login(credentials: String): LineageUser { try { - val usernamePassword = unauthenticated(credentials.username, credentials.password) - val authentication = authenticationManager.authenticate(usernamePassword) + val (userName, password) = getCredentials(credentials) + val unauthenticatedToken = unauthenticated(userName, password) + val authentication = authenticationManager.authenticate(unauthenticatedToken) val user = authentication.principal as LineageUser user.token = "Bearer " + generateToken(authentication) return user + } catch (e: IllegalArgumentException) { + throw ResponseStatusException(BAD_REQUEST, e.message) } catch (e: AuthenticationException) { throw ResponseStatusException(FORBIDDEN, "username or password invalid") } } + private fun getCredentials(credentials: String): Pair { + if (!credentials.contains("Basic")) { + throw IllegalArgumentException("Wrong authentication schema") + } + var userNamePassword = credentials.substringAfter("Basic ") + userNamePassword = String(Base64.getDecoder().decode(userNamePassword), UTF_8) + val userName = userNamePassword.substringBefore(":") + val password = userNamePassword.substringAfter(":") + return Pair(userName, password) + } + private fun generateToken(authentication: Authentication): String { val tokenLive = LocalDateTime.now().plusMinutes(expiration.toLong()) val expiration = Date.from(tokenLive.atZone(ZoneId.systemDefault()).toInstant()) diff --git a/webapp/src/main/kotlin/com/koldyr/genealogy/util/GlobalExceptionHandler.kt b/webapp/src/main/kotlin/com/koldyr/genealogy/util/GlobalExceptionHandler.kt index a042c2f..c6ed8d1 100644 --- a/webapp/src/main/kotlin/com/koldyr/genealogy/util/GlobalExceptionHandler.kt +++ b/webapp/src/main/kotlin/com/koldyr/genealogy/util/GlobalExceptionHandler.kt @@ -2,6 +2,9 @@ package com.koldyr.genealogy.util import java.time.Instant import jakarta.persistence.EntityNotFoundException +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import jakarta.validation.ConstraintViolationException import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.dao.DataAccessException @@ -40,7 +43,6 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() { public override fun handleMethodArgumentNotValid( ex: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest): ResponseEntity { - LOGGER.error(ex.message, ex) val errors = ex.allErrors.map { error -> "${error.objectName}.${error.defaultMessage}" @@ -52,7 +54,6 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() { public override fun handleHandlerMethodValidationException( ex: HandlerMethodValidationException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest): ResponseEntity { - LOGGER.error(ex.message, ex) val errors = ArrayList() for (violation in ex.allValidationResults) { @@ -67,9 +68,22 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() { return badRequest().body(model) } + @ExceptionHandler(ConstraintViolationException::class) + fun handleConstraintViolationException(ex: ConstraintViolationException, request: HttpServletRequest, response: HttpServletResponse): ResponseEntity { + val errors = ex.constraintViolations.map { error -> + error.message + } + + response.sendError(BAD_REQUEST.value(), errors.first()) + + val uri = request.requestURI + val model = buildModel(BAD_REQUEST, uri, errors) + return badRequest().body(model) + } + @ExceptionHandler(EntityNotFoundException::class) - fun handleEntityExceptions(ex: EntityNotFoundException, request: WebRequest): ResponseEntity { - val uri = (request as ServletWebRequest).request.requestURI + fun handleEntityExceptions(ex: EntityNotFoundException, request: HttpServletRequest): ResponseEntity { + val uri = request.requestURI val message = ex.message!! val model = buildModel(NOT_FOUND, uri, message) return ResponseEntity.status(NOT_FOUND).body(model) @@ -86,7 +100,7 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() { @ExceptionHandler(Throwable::class) @ResponseStatus(INTERNAL_SERVER_ERROR) - fun handleAllExceptions(ex: Throwable, request: WebRequest): Map { + fun handleAllExceptions(ex: Throwable, request: HttpServletRequest): Map { LOGGER.error(ex.message, ex) val message = when (ex) { @@ -101,7 +115,7 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() { } } - val uri = (request as ServletWebRequest).request.requestURI + val uri = request.requestURI val model = buildModel(INTERNAL_SERVER_ERROR, uri, message) return model } diff --git a/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/BaseControllerTest.kt b/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/BaseControllerTest.kt index 7827f68..07e942b 100644 --- a/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/BaseControllerTest.kt +++ b/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/BaseControllerTest.kt @@ -2,6 +2,7 @@ package com.koldyr.genealogy.controllers import java.lang.Long.parseLong import java.time.LocalDate +import kotlin.text.Charsets.UTF_8 import com.fasterxml.jackson.databind.ObjectMapper import org.apache.commons.lang3.RandomStringUtils.randomAlphabetic import org.hamcrest.Matchers.matchesRegex @@ -160,9 +161,10 @@ abstract class BaseControllerTest { protected fun login(credentials: Credentials): String { return mockMvc.post("/api/v1/user/login") { - content = mapper.writeValueAsString(credentials) - contentType = APPLICATION_JSON accept = APPLICATION_JSON + headers { + setBasicAuth(credentials.username, credentials.password, UTF_8) + } } .andExpect { status { isOk() } diff --git a/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/UserControllerTest.kt b/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/UserControllerTest.kt index f31ecb5..3536479 100644 --- a/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/UserControllerTest.kt +++ b/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/UserControllerTest.kt @@ -1,5 +1,6 @@ package com.koldyr.genealogy.controllers +import org.apache.commons.lang3.RandomStringUtils import org.hamcrest.CoreMatchers.`is` import org.hamcrest.Matchers.containsString import org.junit.Test @@ -56,15 +57,14 @@ class UserControllerTest : BaseControllerTest() { @Test fun wrongPassword() { - val credentials = Credentials().apply { - username = "me@koldyr.com" - password = "1112" - } + val username = "me@koldyr.com" + var password = "1112" mockMvc.post("/api/v1/user/login") { - content = mapper.writeValueAsString(credentials) - contentType = APPLICATION_JSON accept = APPLICATION_JSON + headers { + setBasicAuth(username, password, Charsets.UTF_8) + } } // .andDo { print() } .andExpect { @@ -73,19 +73,34 @@ class UserControllerTest : BaseControllerTest() { reason("username or password invalid") } } + + password = RandomStringUtils.randomAlphabetic(260) + + mockMvc.post("/api/v1/user/login") { + accept = APPLICATION_JSON + headers { + setBasicAuth(username, password, Charsets.UTF_8) + } + } +// .andDo { print() } + .andExpect { + status { + isBadRequest() + reason("size must be between 0 and 256") + } + } } @Test fun wrongUser() { - val credentials = Credentials().apply { - username = "you@koldyr.com" - password = "koldyr" - } + val username = "you@koldyr.com" + val password = testPassword mockMvc.post("/api/v1/user/login") { - content = mapper.writeValueAsString(credentials) - contentType = APPLICATION_JSON accept = APPLICATION_JSON + headers { + setBasicAuth(username, password, Charsets.UTF_8) + } } // .andDo { print() } .andExpect {