Skip to content

Commit

Permalink
Improved user login
Browse files Browse the repository at this point in the history
  • Loading branch information
koldyr committed Jan 20, 2024
1 parent b196d22 commit a289d22
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 41 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.7</version>
<version>1.4.12</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
Expand Down
14 changes: 8 additions & 6 deletions webapp/person.http
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,7 +34,7 @@ class UserController(
}

@PostMapping("/login")
fun login(@RequestBody @Valid credentials: Credentials): ResponseEntity<UserDTO> {
fun login(@RequestHeader(AUTHORIZATION) @Size(max = 256) credentials: String): ResponseEntity<UserDTO> {
val user = userService.login(credentials)

return ok()
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<String, String> {
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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -40,7 +43,6 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() {

public override fun handleMethodArgumentNotValid(
ex: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest): ResponseEntity<Any> {
LOGGER.error(ex.message, ex)

val errors = ex.allErrors.map { error ->
"${error.objectName}.${error.defaultMessage}"
Expand All @@ -52,7 +54,6 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() {

public override fun handleHandlerMethodValidationException(
ex: HandlerMethodValidationException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest): ResponseEntity<Any> {
LOGGER.error(ex.message, ex)

val errors = ArrayList<ErrorDetails>()
for (violation in ex.allValidationResults) {
Expand All @@ -67,9 +68,22 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() {
return badRequest().body(model)
}

@ExceptionHandler(ConstraintViolationException::class)
fun handleConstraintViolationException(ex: ConstraintViolationException, request: HttpServletRequest, response: HttpServletResponse): ResponseEntity<Any> {
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<Any> {
val uri = (request as ServletWebRequest).request.requestURI
fun handleEntityExceptions(ex: EntityNotFoundException, request: HttpServletRequest): ResponseEntity<Any> {
val uri = request.requestURI
val message = ex.message!!
val model = buildModel(NOT_FOUND, uri, message)
return ResponseEntity.status(NOT_FOUND).body(model)
Expand All @@ -86,7 +100,7 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() {

@ExceptionHandler(Throwable::class)
@ResponseStatus(INTERNAL_SERVER_ERROR)
fun handleAllExceptions(ex: Throwable, request: WebRequest): Map<String, Any> {
fun handleAllExceptions(ex: Throwable, request: HttpServletRequest): Map<String, Any> {
LOGGER.error(ex.message, ex)

val message = when (ex) {
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() }
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down

0 comments on commit a289d22

Please sign in to comment.