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 {