From 134ae88bd3257fb236f10f4d92cd67426f033099 Mon Sep 17 00:00:00 2001 From: Andrey Kassaev Date: Mon, 18 Dec 2023 22:33:37 +0400 Subject: [PATCH] added users, jwt AT and RT --- build.gradle.kts | 7 ++ .../com/kassaev/notes/config/Configuration.kt | 39 +++++++++++ .../notes/config/JwtAuthenticationFilter.kt | 58 ++++++++++++++++ .../com/kassaev/notes/config/JwtProperties.kt | 10 +++ .../notes/config/SecurityConfiguration.kt | 43 ++++++++++++ .../notes/controller/auth/AuthController.kt | 33 +++++++++ .../controller/auth/AuthenticationRequest.kt | 6 ++ .../controller/auth/AuthenticationResponse.kt | 6 ++ .../controller/auth/RefreshTokenRequest.kt | 5 ++ .../notes/controller/auth/TokenResponse.kt | 5 ++ .../controller/{ => note}/NoteController.kt | 2 +- .../notes/controller/user/UserController.kt | 67 +++++++++++++++++++ .../notes/controller/user/UserRequest.kt | 6 ++ .../notes/controller/user/UserResponse.kt | 8 +++ .../kotlin/com/kassaev/notes/model/User.kt | 14 ++++ .../repository/RefreshTokenRepository.kt | 17 +++++ .../notes/repository/UserRepository.kt | 58 ++++++++++++++++ .../notes/service/AuthenticationService.kt | 67 +++++++++++++++++++ .../notes/service/CustomUserDetailsService.kt | 26 +++++++ .../com/kassaev/notes/service/TokenService.kt | 58 ++++++++++++++++ .../com/kassaev/notes/service/UserService.kt | 30 +++++++++ src/main/resources/application.properties | 1 - src/main/resources/application.yaml | 4 ++ 23 files changed, 568 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/com/kassaev/notes/config/Configuration.kt create mode 100644 src/main/kotlin/com/kassaev/notes/config/JwtAuthenticationFilter.kt create mode 100644 src/main/kotlin/com/kassaev/notes/config/JwtProperties.kt create mode 100644 src/main/kotlin/com/kassaev/notes/config/SecurityConfiguration.kt create mode 100644 src/main/kotlin/com/kassaev/notes/controller/auth/AuthController.kt create mode 100644 src/main/kotlin/com/kassaev/notes/controller/auth/AuthenticationRequest.kt create mode 100644 src/main/kotlin/com/kassaev/notes/controller/auth/AuthenticationResponse.kt create mode 100644 src/main/kotlin/com/kassaev/notes/controller/auth/RefreshTokenRequest.kt create mode 100644 src/main/kotlin/com/kassaev/notes/controller/auth/TokenResponse.kt rename src/main/kotlin/com/kassaev/notes/controller/{ => note}/NoteController.kt (96%) create mode 100644 src/main/kotlin/com/kassaev/notes/controller/user/UserController.kt create mode 100644 src/main/kotlin/com/kassaev/notes/controller/user/UserRequest.kt create mode 100644 src/main/kotlin/com/kassaev/notes/controller/user/UserResponse.kt create mode 100644 src/main/kotlin/com/kassaev/notes/model/User.kt create mode 100644 src/main/kotlin/com/kassaev/notes/repository/RefreshTokenRepository.kt create mode 100644 src/main/kotlin/com/kassaev/notes/repository/UserRepository.kt create mode 100644 src/main/kotlin/com/kassaev/notes/service/AuthenticationService.kt create mode 100644 src/main/kotlin/com/kassaev/notes/service/CustomUserDetailsService.kt create mode 100644 src/main/kotlin/com/kassaev/notes/service/TokenService.kt create mode 100644 src/main/kotlin/com/kassaev/notes/service/UserService.kt delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yaml diff --git a/build.gradle.kts b/build.gradle.kts index bcac1f0..98000a5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,13 @@ dependencies { compileOnly("org.projectlombok:lombok:1.18.30") annotationProcessor("org.projectlombok:lombok:1.18.30") + + + implementation ("org.springframework.boot:spring-boot-starter-security") + testImplementation("org.springframework.security:spring-security-test") + implementation("io.jsonwebtoken:jjwt-api:0.12.3") + implementation("io.jsonwebtoken:jjwt-impl:0.12.3") + implementation("io.jsonwebtoken:jjwt-jackson:0.12.3") } tasks.withType { diff --git a/src/main/kotlin/com/kassaev/notes/config/Configuration.kt b/src/main/kotlin/com/kassaev/notes/config/Configuration.kt new file mode 100644 index 0000000..f371929 --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/config/Configuration.kt @@ -0,0 +1,39 @@ +package com.kassaev.notes.config + +import com.kassaev.notes.repository.UserRepository +import com.kassaev.notes.service.CustomUserDetailsService +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.AuthenticationProvider +import org.springframework.security.authentication.dao.DaoAuthenticationProvider +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder + +@Configuration +@EnableConfigurationProperties(JwtProperties::class) +class Configuration { + + @Bean + fun userDetailsService(userRepository: UserRepository): UserDetailsService = + CustomUserDetailsService(userRepository) + + @Bean + fun encoder(): PasswordEncoder = BCryptPasswordEncoder() + + @Bean + fun authenticationProvider(userRepository: UserRepository): AuthenticationProvider = + DaoAuthenticationProvider() + .also { + it.setUserDetailsService(userDetailsService(userRepository)) + it.setPasswordEncoder(encoder()) + } + + @Bean + fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager = + config.authenticationManager + +} \ No newline at end of file diff --git a/src/main/kotlin/com/kassaev/notes/config/JwtAuthenticationFilter.kt b/src/main/kotlin/com/kassaev/notes/config/JwtAuthenticationFilter.kt new file mode 100644 index 0000000..8a71c21 --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/config/JwtAuthenticationFilter.kt @@ -0,0 +1,58 @@ +package com.kassaev.notes.config + +import com.kassaev.notes.service.CustomUserDetailsService +import com.kassaev.notes.service.TokenService +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +class JwtAuthenticationFilter( + private val userDetailsService: CustomUserDetailsService, + private val tokenService: TokenService +): OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val authHeader: String? = request.getHeader("Authorization") + + if(authHeader.doNotContainBearerToken()){ + filterChain.doFilter(request, response) + return + } + + val jwtToken = authHeader!!.extractTokenValue() + val email = tokenService.extractEmail(jwtToken) + + if (email != null && SecurityContextHolder.getContext().authentication == null){ + val foundUser = userDetailsService.loadUserByUsername(email) + + if (tokenService.isValid(jwtToken, foundUser)){ + updateContext(foundUser, request) + } + + filterChain.doFilter(request, response) + } + } + + private fun updateContext(foundUser: UserDetails, request: HttpServletRequest) { + val authToken = UsernamePasswordAuthenticationToken(foundUser, null, foundUser.authorities) + authToken.details = WebAuthenticationDetailsSource().buildDetails(request) + + SecurityContextHolder.getContext().authentication = authToken + } + + private fun String?.doNotContainBearerToken(): Boolean = + this == null || !this.startsWith("Bearer ") + + private fun String.extractTokenValue(): String = + this.substringAfter("Bearer ") +} \ No newline at end of file diff --git a/src/main/kotlin/com/kassaev/notes/config/JwtProperties.kt b/src/main/kotlin/com/kassaev/notes/config/JwtProperties.kt new file mode 100644 index 0000000..d7e275b --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/config/JwtProperties.kt @@ -0,0 +1,10 @@ +package com.kassaev.notes.config + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("jwt") +data class JwtProperties( + val key: String, + val accessTokenExpiration: Long, + val refreshTokenExpiration: Long +) diff --git a/src/main/kotlin/com/kassaev/notes/config/SecurityConfiguration.kt b/src/main/kotlin/com/kassaev/notes/config/SecurityConfiguration.kt new file mode 100644 index 0000000..f3c2c58 --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/config/SecurityConfiguration.kt @@ -0,0 +1,43 @@ +package com.kassaev.notes.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpMethod +import org.springframework.security.authentication.AuthenticationProvider +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.web.DefaultSecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + +@Configuration +@EnableWebSecurity +class SecurityConfiguration( + private val authenticationProvider: AuthenticationProvider +) { + + @Bean + fun securityFilterChain( + http: HttpSecurity, + jwtAuthenticationFilter: JwtAuthenticationFilter + ): DefaultSecurityFilterChain = + http + .csrf { it.disable() } + .authorizeHttpRequests { + it + .requestMatchers("/api/v1.0.0/auth", "/api/v1.0.0/auth/refresh", "/error") + .permitAll() + .requestMatchers(HttpMethod.POST, "/api/v1.0.0/user") + .permitAll() + .requestMatchers("/api/v1.0.0/user**") + .hasRole("ADMIN") + .anyRequest() + .fullyAuthenticated() + } + .sessionManagement { + it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } + .authenticationProvider(authenticationProvider) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java) + .build() +} \ No newline at end of file diff --git a/src/main/kotlin/com/kassaev/notes/controller/auth/AuthController.kt b/src/main/kotlin/com/kassaev/notes/controller/auth/AuthController.kt new file mode 100644 index 0000000..917e36c --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/controller/auth/AuthController.kt @@ -0,0 +1,33 @@ +package com.kassaev.notes.controller.auth + +import com.kassaev.notes.service.AuthenticationService +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.server.ResponseStatusException + +@RestController +@RequestMapping("/api/v1.0.0/auth") +class AuthController( + private val authenticationService: AuthenticationService +) { + + @PostMapping + fun authenticate(@RequestBody authRequest: AuthenticationRequest): AuthenticationResponse = + authenticationService.authentication(authRequest) + + @PostMapping("/refresh") + fun refreshAccessToken( + @RequestBody request: RefreshTokenRequest + ): TokenResponse = + authenticationService.refreshAccessToken(request.token) + ?.mapToTokenResponse() + ?: throw ResponseStatusException(HttpStatus.FORBIDDEN, "Invalid refresh token!") + + private fun String.mapToTokenResponse(): TokenResponse = + TokenResponse( + token = this + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/kassaev/notes/controller/auth/AuthenticationRequest.kt b/src/main/kotlin/com/kassaev/notes/controller/auth/AuthenticationRequest.kt new file mode 100644 index 0000000..d31f360 --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/controller/auth/AuthenticationRequest.kt @@ -0,0 +1,6 @@ +package com.kassaev.notes.controller.auth + +data class AuthenticationRequest( + val email: String, + val password: String +) diff --git a/src/main/kotlin/com/kassaev/notes/controller/auth/AuthenticationResponse.kt b/src/main/kotlin/com/kassaev/notes/controller/auth/AuthenticationResponse.kt new file mode 100644 index 0000000..b043d26 --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/controller/auth/AuthenticationResponse.kt @@ -0,0 +1,6 @@ +package com.kassaev.notes.controller.auth + +data class AuthenticationResponse( + val accessToken: String, + val refreshToken: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/kassaev/notes/controller/auth/RefreshTokenRequest.kt b/src/main/kotlin/com/kassaev/notes/controller/auth/RefreshTokenRequest.kt new file mode 100644 index 0000000..c649e26 --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/controller/auth/RefreshTokenRequest.kt @@ -0,0 +1,5 @@ +package com.kassaev.notes.controller.auth + +data class RefreshTokenRequest( + val token: String +) diff --git a/src/main/kotlin/com/kassaev/notes/controller/auth/TokenResponse.kt b/src/main/kotlin/com/kassaev/notes/controller/auth/TokenResponse.kt new file mode 100644 index 0000000..a76bb5e --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/controller/auth/TokenResponse.kt @@ -0,0 +1,5 @@ +package com.kassaev.notes.controller.auth + +data class TokenResponse( + val token: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/kassaev/notes/controller/NoteController.kt b/src/main/kotlin/com/kassaev/notes/controller/note/NoteController.kt similarity index 96% rename from src/main/kotlin/com/kassaev/notes/controller/NoteController.kt rename to src/main/kotlin/com/kassaev/notes/controller/note/NoteController.kt index cb6f343..2e055d0 100644 --- a/src/main/kotlin/com/kassaev/notes/controller/NoteController.kt +++ b/src/main/kotlin/com/kassaev/notes/controller/note/NoteController.kt @@ -1,4 +1,4 @@ -package com.kassaev.notes.controller +package com.kassaev.notes.controller.note import com.kassaev.notes.model.Note import com.kassaev.notes.service.NoteService diff --git a/src/main/kotlin/com/kassaev/notes/controller/user/UserController.kt b/src/main/kotlin/com/kassaev/notes/controller/user/UserController.kt new file mode 100644 index 0000000..a3528d4 --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/controller/user/UserController.kt @@ -0,0 +1,67 @@ +package com.kassaev.notes.controller.user + +import com.kassaev.notes.model.Role +import com.kassaev.notes.model.User +import com.kassaev.notes.service.UserService +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.server.ResponseStatusException +import java.util.UUID + +@RestController +@RequestMapping("/api/v1.0.0/user") +class UserController( + private val userService: UserService +) { + + @PostMapping + fun create(@RequestBody userRequest: UserRequest): UserResponse = + userService.createUser( + user = userRequest.toModel() + ) + ?.toResponse() + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot create user.") + + @GetMapping + fun listAll(): List = + userService.findAll() + .map { it.toResponse() } + + @GetMapping("/{uuid}") + fun findByUUID(@PathVariable uuid: UUID): UserResponse = + userService.findByUUID(uuid) + ?.toResponse() + ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Cannot find a user.") + + @DeleteMapping("/{uuid}") + fun deleteByUUID(@PathVariable uuid: UUID): ResponseEntity { + val success = userService.deleteByUUID(uuid) + + return if(success) { + ResponseEntity.noContent().build() + } else { + throw ResponseStatusException(HttpStatus.NOT_FOUND, "Cannot find a user.") + } + } + + private fun UserRequest.toModel(): User = + User( + id = UUID.randomUUID(), + email = this.email, + password = this.password, + role = Role.USER + ) + + private fun User.toResponse(): UserResponse = + UserResponse( + uuid = this.id, + email = this.email + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/kassaev/notes/controller/user/UserRequest.kt b/src/main/kotlin/com/kassaev/notes/controller/user/UserRequest.kt new file mode 100644 index 0000000..e9937db --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/controller/user/UserRequest.kt @@ -0,0 +1,6 @@ +package com.kassaev.notes.controller.user + +data class UserRequest( + val email: String, + val password: String +) diff --git a/src/main/kotlin/com/kassaev/notes/controller/user/UserResponse.kt b/src/main/kotlin/com/kassaev/notes/controller/user/UserResponse.kt new file mode 100644 index 0000000..d6d05fe --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/controller/user/UserResponse.kt @@ -0,0 +1,8 @@ +package com.kassaev.notes.controller.user + +import java.util.UUID + +data class UserResponse( + val uuid: UUID, + val email: String +) diff --git a/src/main/kotlin/com/kassaev/notes/model/User.kt b/src/main/kotlin/com/kassaev/notes/model/User.kt new file mode 100644 index 0000000..f716c7c --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/model/User.kt @@ -0,0 +1,14 @@ +package com.kassaev.notes.model + +import java.util.UUID + +data class User( + val id: UUID, + val email: String, + val password: String, + val role: Role +) + +enum class Role{ + USER, ADMIN +} diff --git a/src/main/kotlin/com/kassaev/notes/repository/RefreshTokenRepository.kt b/src/main/kotlin/com/kassaev/notes/repository/RefreshTokenRepository.kt new file mode 100644 index 0000000..74155fd --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/repository/RefreshTokenRepository.kt @@ -0,0 +1,17 @@ +package com.kassaev.notes.repository + +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.stereotype.Repository + +@Repository +class RefreshTokenRepository { + + private val tokens = mutableMapOf() + + fun findUserDetailsByToken(token: String): UserDetails? = + tokens[token] + + fun save(token: String, userDetails: UserDetails) { + tokens[token] = userDetails + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/kassaev/notes/repository/UserRepository.kt b/src/main/kotlin/com/kassaev/notes/repository/UserRepository.kt new file mode 100644 index 0000000..dca5f4d --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/repository/UserRepository.kt @@ -0,0 +1,58 @@ +package com.kassaev.notes.repository + +import com.kassaev.notes.model.Role +import com.kassaev.notes.model.User +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Repository +import java.util.* + +@Repository +class UserRepository( + private val encoder: PasswordEncoder +) { + private val users = mutableListOf( + User( + id = UUID.randomUUID(), + email = "user1@mail.com", + password = encoder.encode("pass1"), + role = Role.USER + ), + User( + id = UUID.randomUUID(), + email = "user2@mail.com", + password = encoder.encode("pass2"), + role = Role.USER + ), + User( + id = UUID.randomUUID(), + email = "user3@mail.com", + password = encoder.encode("pass3"), + role = Role.ADMIN + ), + ) + + fun save(user: User): Boolean { + val updated = user.copy(password = encoder.encode( + user.password + )) + return users.add(updated) + } + + fun findByEmail(email: String): User? = + users.firstOrNull { it.email == email } + + fun findByUUID(uuid: UUID): User? = + users.firstOrNull { it.id == uuid } + + fun findAll(): List = + users + + fun deleteByUUID(uuid: UUID): Boolean { + val foundUser = findByUUID(uuid) + + return foundUser?.let { + users.remove(it) + } ?: false + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/kassaev/notes/service/AuthenticationService.kt b/src/main/kotlin/com/kassaev/notes/service/AuthenticationService.kt new file mode 100644 index 0000000..52844a2 --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/service/AuthenticationService.kt @@ -0,0 +1,67 @@ +package com.kassaev.notes.service + +import com.kassaev.notes.config.JwtProperties +import com.kassaev.notes.controller.auth.AuthenticationRequest +import com.kassaev.notes.controller.auth.AuthenticationResponse +import com.kassaev.notes.repository.RefreshTokenRepository +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.stereotype.Service +import java.util.Date + +@Service +class AuthenticationService( + private val authManager: AuthenticationManager, + private val userDetailsService: CustomUserDetailsService, + private val tokenService: TokenService, + private val jwtProperties: JwtProperties, + private val refreshTokenRepository: RefreshTokenRepository +) { + fun authentication(authRequest: AuthenticationRequest): AuthenticationResponse { + authManager.authenticate( + UsernamePasswordAuthenticationToken( + authRequest.email, + authRequest.password + ) + ) + + val user = userDetailsService.loadUserByUsername(authRequest.email) + + val accessToken = generateAccessToken(user) + val refreshToken = generateRefreshToken(user) + + refreshTokenRepository.save(refreshToken, user) + + return AuthenticationResponse( + accessToken = accessToken, + refreshToken = refreshToken + ) + } + + private fun generateRefreshToken(user: UserDetails) = tokenService.generate( + userDetails = user, + expirationDate = Date(System.currentTimeMillis() + jwtProperties.refreshTokenExpiration) + ) + + private fun generateAccessToken(user: UserDetails) = tokenService.generate( + userDetails = user, + expirationDate = Date(System.currentTimeMillis() + jwtProperties.accessTokenExpiration) + ) + + fun refreshAccessToken(token: String): String? { + val extractedEmail = tokenService.extractEmail(token) + + return extractedEmail?.let { email -> + val currentUserDetails = userDetailsService.loadUserByUsername(email) + val refreshTokenUserDetails = refreshTokenRepository.findUserDetailsByToken(token) + + if (!tokenService.isExpired(token) && currentUserDetails.username == refreshTokenUserDetails?.username) + // TODO: check if this refresh token is the same token in db for this user . If so generate new access token and refresh token and update record in with newly created refresh token for this user. + generateAccessToken(currentUserDetails) + else + null + } + } + +} diff --git a/src/main/kotlin/com/kassaev/notes/service/CustomUserDetailsService.kt b/src/main/kotlin/com/kassaev/notes/service/CustomUserDetailsService.kt new file mode 100644 index 0000000..667d609 --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/service/CustomUserDetailsService.kt @@ -0,0 +1,26 @@ +package com.kassaev.notes.service + +import com.kassaev.notes.repository.UserRepository +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.stereotype.Service + +typealias ApplicationUser = com.kassaev.notes.model.User +@Service +class CustomUserDetailsService( + private val userRepository: UserRepository +): UserDetailsService { + override fun loadUserByUsername(username: String): UserDetails = + userRepository.findByEmail(username) + ?.mapToUserDetails() + ?: throw UsernameNotFoundException("Not found!") + + private fun ApplicationUser.mapToUserDetails(): UserDetails = + User.builder() + .username(this.email) + .password(this.password) + .roles(this.role.name) + .build() +} \ No newline at end of file diff --git a/src/main/kotlin/com/kassaev/notes/service/TokenService.kt b/src/main/kotlin/com/kassaev/notes/service/TokenService.kt new file mode 100644 index 0000000..476b6d3 --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/service/TokenService.kt @@ -0,0 +1,58 @@ +package com.kassaev.notes.service + +import com.kassaev.notes.config.JwtProperties +import io.jsonwebtoken.Claims +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.Keys +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.stereotype.Service +import java.util.Date + +@Service +class TokenService( + jwtProperties: JwtProperties +) { + private val secretKey = Keys.hmacShaKeyFor( + jwtProperties.key.toByteArray() + ) + + fun generate( + userDetails: UserDetails, + expirationDate: Date, + additionalClaims: Map = emptyMap() + ): String = + Jwts.builder() + .claims() + .subject(userDetails.username) + .issuedAt(Date(System.currentTimeMillis())) + .expiration(expirationDate) + .add(additionalClaims) + .and() + .signWith(secretKey) + .compact() + + fun extractEmail(token: String): String? = + getAllClaims(token) + .subject + + fun isExpired(token: String): Boolean = + getAllClaims(token) + .expiration + .before(Date(System.currentTimeMillis())) + + fun isValid(token: String, userDetails: UserDetails): Boolean { + val email = extractEmail(token) + + return userDetails.username == email && !isExpired(token) + } + + private fun getAllClaims(token: String): Claims { + val parser = Jwts.parser() + .verifyWith(secretKey) + .build() + + return parser + .parseSignedClaims(token) + .payload + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/kassaev/notes/service/UserService.kt b/src/main/kotlin/com/kassaev/notes/service/UserService.kt new file mode 100644 index 0000000..d01067d --- /dev/null +++ b/src/main/kotlin/com/kassaev/notes/service/UserService.kt @@ -0,0 +1,30 @@ +package com.kassaev.notes.service + +import com.kassaev.notes.model.User +import com.kassaev.notes.repository.UserRepository +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +class UserService( + private val userRepository: UserRepository +) { + fun createUser(user: User): User? { + val found = userRepository.findByEmail(user.email) + + return if (found == null) { + userRepository.save(user) + user + } else null + } + + fun findByUUID(uuid: UUID): User? = + userRepository.findByUUID(uuid) + + fun findAll(): List = + userRepository.findAll() + + fun deleteByUUID(uuid: UUID): Boolean = + userRepository.deleteByUUID(uuid) + +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..ddade16 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,4 @@ +jwt: + key: ${JWT_KEY} + access-token-expiration: 3600000 + refresh-token-expiration: 86400000 \ No newline at end of file