Skip to content

Commit

Permalink
Feature/24 prepare backend auth (#37)
Browse files Browse the repository at this point in the history
* add BE auth (FilterChain, loadByUser, authUser, SecurityConfiguration), also add authUser entity.
* add test, changed liquibase authUser emailt to unique, add logout with bean handlers and registrastion with login
---------

Co-authored-by: Štěpán Moc <[email protected]>
  • Loading branch information
MocStepan and MocStepan authored Apr 13, 2024
1 parent e6b5c90 commit 1d4f8c4
Show file tree
Hide file tree
Showing 46 changed files with 1,692 additions and 45 deletions.
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ out/

### custom ###
config/application.yml
*.jpb
31 changes: 24 additions & 7 deletions backend/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.util.regex.Pattern.compile

plugins {
id("org.springframework.boot") version "3.2.4"
id("io.spring.dependency-management") version "1.1.4"
//id("org.jlleitschuh.gradle.ktlint") version "12.1.0"
id("java")
id("jacoco")
kotlin("jvm") version "1.9.23"
Expand All @@ -18,6 +18,12 @@ java {
sourceCompatibility = JavaVersion.VERSION_17
}

configurations {
testImplementation {
exclude(group = "org.mockito") // it is shipped with spring and there is no need, since we use mockk
}
}

repositories {
mavenCentral()
}
Expand All @@ -27,20 +33,31 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-webflux")

implementation("io.github.oshai:kotlin-logging-jvm:5.1.0")

implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
runtimeOnly("org.postgresql:postgresql")
implementation("org.liquibase:liquibase-core:4.26.0")

implementation ("org.postgresql:postgresql")

val jjwtVersion = "0.12.5"
implementation("io.jsonwebtoken:jjwt-api:$jjwtVersion")
runtimeOnly("io.jsonwebtoken:jjwt-impl:$jjwtVersion")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:$jjwtVersion")

val kotestVersion = "5.8.0"
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.0")

compile("org.springframework.boot:spring-boot-starter-webflux")
testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
testImplementation("io.kotest:kotest-property:$kotestVersion")
testImplementation("com.ninja-squad:springmockk:4.0.2")
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.3")
}


tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
Expand All @@ -62,6 +79,7 @@ tasks.jacocoTestReport {
}
dependsOn(tasks.test) // tests are required to run before generating the report
}

tasks.jacocoTestCoverageVerification {
violationRules {
rule {
Expand All @@ -71,4 +89,3 @@ tasks.jacocoTestCoverageVerification {
}
}
}

2 changes: 2 additions & 0 deletions backend/src/main/kotlin/com/tul/backend/BackendApplication.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.tul.backend

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication

@SpringBootApplication
@ConfigurationPropertiesScan
class BackendApplication

fun main(args: Array<String>) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.tul.backend.auth.base.config

import com.tul.backend.auth.base.service.TokenFilterService
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter

@Component
class JwtAuthenticationFilter(
private val tokenFilterService: TokenFilterService

) : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val userDetails = tokenFilterService.validateRequest(request)
if (userDetails != null) {
tokenFilterService.updateContext(userDetails, request, response)
}
filterChain.doFilter(request, response)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.tul.backend.auth.base.config

import com.tul.backend.auth.base.service.CustomPasswordEncoder
import com.tul.backend.auth.base.service.CustomUserDetailsService
import com.tul.backend.auth.repository.AuthUserRepository
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.password.PasswordEncoder
import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler
import org.springframework.security.web.authentication.logout.LogoutHandler
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler


@Configuration
class JwtConfiguration {

@Bean
fun userDetailService(authUserRepository: AuthUserRepository): UserDetailsService =
CustomUserDetailsService(authUserRepository)

@Bean
fun encoder(): PasswordEncoder = CustomPasswordEncoder()

@Bean
fun authenticationProvider(authUserRepository: AuthUserRepository): AuthenticationProvider =
DaoAuthenticationProvider()
.also {
it.setUserDetailsService(userDetailService(authUserRepository))
it.setPasswordEncoder(encoder())
}

@Bean
fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager =
config.authenticationManager

@Bean
fun logoutSuccessHandler(): LogoutSuccessHandler {
val handler = SimpleUrlLogoutSuccessHandler()
handler.setUseReferer(true)
return handler
}

@Bean
fun cookieClearingLogoutHandler(): LogoutHandler {
return CookieClearingLogoutHandler("access_token")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.tul.backend.auth.base.config

import com.fasterxml.jackson.databind.ObjectMapper
import com.tul.backend.auth.base.dto.ErrorDTO
import com.tul.backend.auth.base.valueobject.AuthUserRole
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.DefaultSecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.authentication.logout.LogoutHandler
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler


@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfiguration(
private val objectMapper: ObjectMapper,
private val authenticationProvider: AuthenticationProvider,
private val logoutSuccessHandler: LogoutSuccessHandler,
private val cookieClearingLogoutHandler: LogoutHandler
) {
private val userUnsecuredEndpoints =
arrayOf(
"/api/auth/login",
"/api/auth/register",
)

private val adminUnsecuredEndpoints =
arrayOf(
"api/auth/test"
)

@Bean
fun securityFilterChain(
http: HttpSecurity,
jwtAuthenticationFilter: JwtAuthenticationFilter
): DefaultSecurityFilterChain =
http
.csrf { it.disable() }
.cors { it.disable() }
.authorizeHttpRequests {
it
.requestMatchers(*userUnsecuredEndpoints).permitAll()
.requestMatchers(*adminUnsecuredEndpoints).hasRole(AuthUserRole.ADMIN.name)
.anyRequest().fullyAuthenticated()
}
.sessionManagement { session: SessionManagementConfigurer<HttpSecurity> ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
.exceptionHandling {
it.authenticationEntryPoint(authenticationExceptionHandler)
}
.logout {
it.logoutUrl("/api/auth/logout")
.addLogoutHandler(cookieClearingLogoutHandler)
.logoutSuccessHandler(logoutSuccessHandler)
.deleteCookies("access_token")
.permitAll()
}
.build()

private val authenticationExceptionHandler =
{ _: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException ->
response.contentType = MediaType.APPLICATION_JSON_VALUE
response.status = HttpServletResponse.SC_UNAUTHORIZED
objectMapper.writeValue(response.writer, ErrorDTO("${authException.message}"))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.tul.backend.auth.base.dto

data class ErrorDTO(
val message: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.tul.backend.auth.base.service

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Component

@Component
class CustomPasswordEncoder(
private val encoder: BCryptPasswordEncoder = BCryptPasswordEncoder(),
) : PasswordEncoder {

override fun encode(rawPassword: CharSequence?): String {
return encoder.encode(rawPassword)
}

override fun matches(rawPassword: CharSequence?, encodedPassword: String?): Boolean {
return encoder.matches(rawPassword, encodedPassword)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.tul.backend.auth.base.service

import com.tul.backend.auth.entity.AuthUser
import com.tul.backend.auth.repository.AuthUserRepository
import jakarta.transaction.Transactional
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.stereotype.Service

@Service
@Transactional
class CustomUserDetailsService(
private val authUserRepository: AuthUserRepository
) : UserDetailsService {
override fun loadUserByUsername(email: String?): UserDetails {
return authUserRepository.findByEmail(email!!)
?.mapToUserDetails()
?: throw Exception("User not found")
}

private fun AuthUser.mapToUserDetails(): UserDetails {
return User.builder()
.username(email.value)
.password(password)
.roles(this.role.name)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.tul.backend.auth.base.service

import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.stereotype.Component

@Component
class TokenFilterService(
@Qualifier("customUserDetailsService") private val userDetailsService: UserDetailsService,
private val tokenService: TokenService
) {

fun validateRequest(request: HttpServletRequest): UserDetails? {
val (email, token) = extractEmailAndToken(request)

if (email != null && SecurityContextHolder.getContext().authentication == null) {
val userDetails = userDetailsService.loadUserByUsername(email)
if (tokenService.isValidToken(token, userDetails)) {
return userDetails
}
}
return null
}

fun updateContext(
userDetails: UserDetails,
request: HttpServletRequest,
response: HttpServletResponse,
) {
val authToken = UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.authorities
)
authToken.details = WebAuthenticationDetailsSource().buildDetails(request)

SecurityContextHolder.getContext().authentication = authToken
}

private fun extractEmailAndToken(request: HttpServletRequest): Pair<String?, String?> {
val authHeader = request.getHeader("Cookie")
var token: String? = null
var email: String? = null

if (authHeader != null && authHeader.startsWith("access_token=")) {
token = authHeader.substring(13)
email = tokenService.extractEmail(token)
}
return Pair(email, token)
}
}
Loading

0 comments on commit 1d4f8c4

Please sign in to comment.