diff --git a/build.gradle.kts b/build.gradle.kts index f5ad0de..52600c6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,9 +4,10 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.springframework.boot") version "2.3.0.RELEASE" id("io.spring.dependency-management") version "1.0.9.RELEASE" - kotlin("jvm") version "1.3.71" + kotlin("jvm") version "1.3.72" kotlin("plugin.spring") version "1.3.72" kotlin("plugin.jpa") version "1.3.72" + kotlin("plugin.serialization") version "1.3.70" } group = "com.aitrainer" @@ -22,12 +23,18 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-aop") implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.3.0.RELEASE") + implementation("org.springframework.security.oauth:spring-security-oauth2:2.5.0.RELEASE") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.apache.logging.log4j:log4j-core:2.13.3") implementation("org.apache.logging.log4j:log4j-api:2.13.3") implementation("org.slf4j:slf4j-api:1.7.30") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") // JVM dependency + implementation("io.jsonwebtoken:jjwt:0.9.1") + runtimeOnly("mysql:mysql-connector-java") diff --git a/data/db/update_0_0_3.sql b/data/db/update_0_0_3.sql new file mode 100644 index 0000000..4e4cbfb --- /dev/null +++ b/data/db/update_0_0_3.sql @@ -0,0 +1 @@ +INSERT INTO `customer` (`name`, `firstname`, `email`, `password`, `sex`, `age`, `active`, `date_add`, `date_change`, `data_policy_allowed`, `admin`) VALUES ('Dummy User', NULL, 'bosi', '$2a$10$thOc8jS750c7xe9U9Qq3GuSPs/H0Pt2Ads05yzUlyzQBIj.Rk9QCy', 'm', 40, 'N', NULL, NULL, 1, 1); diff --git a/src/main/kotlin/com/aitrainer/api/ApiApplication.kt b/src/main/kotlin/com/aitrainer/api/ApiApplication.kt index 0682bc7..00ffe92 100644 --- a/src/main/kotlin/com/aitrainer/api/ApiApplication.kt +++ b/src/main/kotlin/com/aitrainer/api/ApiApplication.kt @@ -4,11 +4,8 @@ import org.slf4j.LoggerFactory import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.builder.SpringApplicationBuilder -import org.springframework.web.bind.annotation.RestController - @SpringBootApplication -@RestController class ApiApplication private val logger = LoggerFactory.getLogger(ApiApplication::class.simpleName) diff --git a/src/main/kotlin/com/aitrainer/api/controller/ApplicationProperties.kt b/src/main/kotlin/com/aitrainer/api/controller/ApplicationProperties.kt index 92070d1..927c727 100644 --- a/src/main/kotlin/com/aitrainer/api/controller/ApplicationProperties.kt +++ b/src/main/kotlin/com/aitrainer/api/controller/ApplicationProperties.kt @@ -6,7 +6,7 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping() +@RequestMapping class ApplicationProperties { @Value("\${application.version}") diff --git a/src/main/kotlin/com/aitrainer/api/controller/CustomerController.kt b/src/main/kotlin/com/aitrainer/api/controller/CustomerController.kt index f9e065b..93d068d 100644 --- a/src/main/kotlin/com/aitrainer/api/controller/CustomerController.kt +++ b/src/main/kotlin/com/aitrainer/api/controller/CustomerController.kt @@ -1,39 +1,53 @@ package com.aitrainer.api.controller import com.aitrainer.api.model.Customer +import com.aitrainer.api.model.User +import com.aitrainer.api.service.ServiceBeans import com.aitrainer.api.repository.CustomerRepository +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders import org.springframework.http.ResponseEntity +import org.springframework.security.access.annotation.Secured import org.springframework.web.bind.annotation.* import javax.validation.Valid + @RestController @RequestMapping("/api") class CustomerController ( private val customerRepository: CustomerRepository ) { - + + @Autowired + var serviceBeans: ServiceBeans? = null + + @Secured @GetMapping("/customers") - fun getAllCustomers(): List = + fun getAllCustomers(@RequestHeader headers: HttpHeaders): List = customerRepository.findAll() + @Secured @PostMapping("/customers") - fun createNewCustomer(@Valid @RequestBody customer: Customer): Customer = + fun createNewCustomer(@Valid @RequestBody customer: Customer, @RequestHeader headers: HttpHeaders): Customer = customerRepository.save(customer) + @Secured @GetMapping("/customers/{id}") - fun getCustomerById(@PathVariable(value = "id") customerId: Long): ResponseEntity { + fun getCustomerById(@PathVariable(value = "id") customerId: Long, @RequestHeader headers: HttpHeaders): ResponseEntity { return customerRepository.findById(customerId).map { customer -> ResponseEntity.ok(customer) }.orElse(ResponseEntity.notFound().build()) - } + } + @Secured @GetMapping("/customers/real") - fun getRealCustomers(active: String): List = + fun getRealCustomers(active: String, @RequestHeader headers: HttpHeaders): List = customerRepository.findByActive(active) - + @Secured @PutMapping("/customers/{id}") fun updateCustomerById(@PathVariable(value = "id") customerId: Long, - @Valid @RequestBody newCustomer: Customer): ResponseEntity { + @Valid @RequestBody newCustomer: Customer, + @RequestHeader headers: HttpHeaders): ResponseEntity { return customerRepository.findById(customerId).map { existingCustomer -> val updatedCustomer: Customer = existingCustomer @@ -43,6 +57,60 @@ class CustomerController ( private val customerRepository: CustomerRepository ) age = newCustomer.age) ResponseEntity.ok().body(customerRepository.save(updatedCustomer)) }.orElse(ResponseEntity.notFound().build()) + } + + + @PostMapping("/registration") + fun registration(@Valid @RequestBody json: String): ResponseEntity<*> { + val customer = Customer() + + val newUser: User = User().fromJson(json) + with (customer) { + email = newUser.username + password = serviceBeans!!.passwordEncoder().encode(newUser.password) + } + + val returnCustomer: Customer? = customerRepository.findByEmail(newUser.username).let { + if ( it == null) { + customerRepository.save(customer) + } else { + null + } + } + + return if ( returnCustomer != null ) { + ResponseEntity.ok().body(returnCustomer) + } else { + ResponseEntity.badRequest().body("Customer exists") + } } + + @GetMapping("/login") + fun login(@Valid @RequestBody json: String): ResponseEntity<*> { + val customer = Customer() + + val newUser: User = User().fromJson(json) + with (customer) { + email = newUser.username + password = newUser.password + } + val returnCustomer: Customer? = customerRepository.findByEmail(newUser.username).let { + if ( it == null) { + null + } else { + if (serviceBeans!!.passwordEncoder().matches(newUser.password, it.password)) { + it + } else { + null + } + + } + } + return if ( returnCustomer != null ) { + ResponseEntity.ok().body(returnCustomer) + } else { + ResponseEntity.badRequest().body("Customer does not exist or the password is wrong") + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/aitrainer/api/controller/CustomerControllerAspect.kt b/src/main/kotlin/com/aitrainer/api/controller/CustomerControllerAspect.kt index b4301e6..656fb28 100644 --- a/src/main/kotlin/com/aitrainer/api/controller/CustomerControllerAspect.kt +++ b/src/main/kotlin/com/aitrainer/api/controller/CustomerControllerAspect.kt @@ -2,15 +2,13 @@ package com.aitrainer.api.controller import com.aitrainer.api.ApiApplication import com.aitrainer.api.repository.ConfigurationRepository -import org.aspectj.lang.annotation.Around +import org.aspectj.lang.JoinPoint import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Before -import org.aspectj.lang.annotation.Pointcut import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component - @Suppress("unused") @Aspect @Component @@ -22,13 +20,11 @@ class CustomerControllerAspect { @Autowired private lateinit var properties: ApplicationProperties - @Suppress("unused") - @Pointcut("execution(* com.aitrainer.api.controller.CustomerController.*())") - fun customerControllerAspect() { - } - - @Before("customerControllerAspect()") - fun loggingAop() { + @Before("execution(* com.aitrainer.api.controller.CustomerController.*(..))") + fun customerControllerAspect(joinPoint: JoinPoint) { + println("customer controller") Singleton.checkDBUpdate(configurationRepository, properties) } -} \ No newline at end of file + +} + diff --git a/src/main/kotlin/com/aitrainer/api/controller/DatabaseCheckSingleton.kt b/src/main/kotlin/com/aitrainer/api/controller/DatabaseCheckSingleton.kt index ec4fc01..6db8eda 100644 --- a/src/main/kotlin/com/aitrainer/api/controller/DatabaseCheckSingleton.kt +++ b/src/main/kotlin/com/aitrainer/api/controller/DatabaseCheckSingleton.kt @@ -3,9 +3,7 @@ package com.aitrainer.api.controller import com.aitrainer.api.ApiApplication import com.aitrainer.api.model.Configuration import com.aitrainer.api.repository.ConfigurationRepository -import org.hibernate.Hibernate.isInitialized import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.RestController import java.io.File import java.sql.Connection @@ -73,8 +71,7 @@ object Singleton { } - fun execSQL( sql: String ) { - + private fun execSQL( sql: String ) { if (! this.initialized ) { this.getConnection() } diff --git a/src/main/kotlin/com/aitrainer/api/controller/ExerciseTypeControllerAspect.kt b/src/main/kotlin/com/aitrainer/api/controller/ExerciseTypeControllerAspect.kt index 67531b3..acbb9ca 100644 --- a/src/main/kotlin/com/aitrainer/api/controller/ExerciseTypeControllerAspect.kt +++ b/src/main/kotlin/com/aitrainer/api/controller/ExerciseTypeControllerAspect.kt @@ -2,7 +2,6 @@ package com.aitrainer.api.controller import com.aitrainer.api.ApiApplication import com.aitrainer.api.repository.ConfigurationRepository -import org.aspectj.lang.annotation.Around import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Before import org.aspectj.lang.annotation.Pointcut diff --git a/src/main/kotlin/com/aitrainer/api/model/Configuration.kt b/src/main/kotlin/com/aitrainer/api/model/Configuration.kt index 0370a6c..de7b2b1 100644 --- a/src/main/kotlin/com/aitrainer/api/model/Configuration.kt +++ b/src/main/kotlin/com/aitrainer/api/model/Configuration.kt @@ -5,7 +5,6 @@ import javax.persistence.GeneratedValue import javax.persistence.GenerationType import javax.persistence.Id import javax.validation.constraints.NotBlank -import javax.validation.constraints.Null @Entity data class Configuration ( diff --git a/src/main/kotlin/com/aitrainer/api/model/Customer.kt b/src/main/kotlin/com/aitrainer/api/model/Customer.kt index 490cd5a..9e3014a 100644 --- a/src/main/kotlin/com/aitrainer/api/model/Customer.kt +++ b/src/main/kotlin/com/aitrainer/api/model/Customer.kt @@ -4,13 +4,10 @@ import javax.persistence.Entity import javax.persistence.GeneratedValue import javax.persistence.GenerationType import javax.persistence.Id -import javax.validation.constraints.NotBlank @Entity data class Customer ( - @get: NotBlank - - var name: String = "", + var name: String = "", var firstname: String = "", var email: String = "", var age: Int = 0, @@ -20,7 +17,8 @@ data class Customer ( var dateChange: String? = null, var dataPolicyAllowed: Int = 0, var admin: Int = 0, - + var password: String = "", + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val customer_id: Long = 0 ) diff --git a/src/main/kotlin/com/aitrainer/api/model/User.kt b/src/main/kotlin/com/aitrainer/api/model/User.kt new file mode 100644 index 0000000..f1f4ce3 --- /dev/null +++ b/src/main/kotlin/com/aitrainer/api/model/User.kt @@ -0,0 +1,15 @@ +package com.aitrainer.api.model + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +@Serializable +data class User ( + var username: String = "", + var password: String = "" +) { + @OptIn(UnstableDefault::class) + fun fromJson(json: String): User { + return Json.parse(serializer(), json) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/aitrainer/api/repository/CustomerRepository.kt b/src/main/kotlin/com/aitrainer/api/repository/CustomerRepository.kt index cf0385d..4e6c10b 100644 --- a/src/main/kotlin/com/aitrainer/api/repository/CustomerRepository.kt +++ b/src/main/kotlin/com/aitrainer/api/repository/CustomerRepository.kt @@ -6,5 +6,7 @@ import org.springframework.stereotype.Repository @Repository interface CustomerRepository : JpaRepository { - fun findByActive( active: String? ):List + fun findByActive( active: String? ): List + + fun findByEmail(email: String?): Customer? } diff --git a/src/main/kotlin/com/aitrainer/api/security/AuthenticationControllerAspect.kt b/src/main/kotlin/com/aitrainer/api/security/AuthenticationControllerAspect.kt new file mode 100644 index 0000000..c106785 --- /dev/null +++ b/src/main/kotlin/com/aitrainer/api/security/AuthenticationControllerAspect.kt @@ -0,0 +1,31 @@ +package com.aitrainer.api.security + +import com.aitrainer.api.ApiApplication +import com.aitrainer.api.controller.ApplicationProperties +import com.aitrainer.api.controller.Singleton +import com.aitrainer.api.repository.ConfigurationRepository +import org.aspectj.lang.JoinPoint +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Before +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component + +@Suppress("unused") +@Aspect +@Component +class AuthenticationControllerAspect { + private val logger = LoggerFactory.getLogger(ApiApplication::class.simpleName) + + @Autowired + private lateinit var configurationRepository: ConfigurationRepository + @Autowired + private lateinit var properties: ApplicationProperties + + @Before("execution(* com.aitrainer.api.security.JwtAuthenticationController.*(..))") + fun customerControllerAspect(joinPoint: JoinPoint) { + println("auth controller join") + Singleton.checkDBUpdate(configurationRepository, properties) + } + +} diff --git a/src/main/kotlin/com/aitrainer/api/security/JwtAuthenticationController.kt b/src/main/kotlin/com/aitrainer/api/security/JwtAuthenticationController.kt new file mode 100644 index 0000000..9064b5a --- /dev/null +++ b/src/main/kotlin/com/aitrainer/api/security/JwtAuthenticationController.kt @@ -0,0 +1,49 @@ +package com.aitrainer.api.security + +import com.aitrainer.api.service.UserDetailsServiceImpl +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.ResponseEntity +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.BadCredentialsException +import org.springframework.security.authentication.DisabledException +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.web.bind.annotation.* +import org.springframework.stereotype.Component + + + +@Component +@RestController +//@CrossOrigin +@RequestMapping("/api") +class JwtAuthenticationController { + @Autowired + private val authenticationManager: AuthenticationManager? = null + + @Autowired + private val jwtTokenUtil: JwtTokenUtil? = null + + @Autowired + private val jwtUserDetailsService: UserDetailsServiceImpl? = null + + @PostMapping("/authenticate") + fun generateAuthenticationToken(@RequestBody authenticationRequest: JwtRequest): ResponseEntity<*> { + + authenticate(authenticationRequest.username!!, authenticationRequest.password!!) + + val userDetails = jwtUserDetailsService + ?.loadUserByUsername(authenticationRequest.username) + val token: String = jwtTokenUtil!!.generateToken(userDetails!!) + return ResponseEntity.ok(JwtResponse(token)) + } + + private fun authenticate(username: String, password: String) { + try { + authenticationManager!!.authenticate(UsernamePasswordAuthenticationToken(username, password)) + } catch (e: DisabledException) { + throw Exception("USER_DISABLED", e) + } catch (e: BadCredentialsException) { + throw Exception("INVALID_CREDENTIALS", e) + } + } +} diff --git a/src/main/kotlin/com/aitrainer/api/security/JwtAuthenticationEntryPoint.kt b/src/main/kotlin/com/aitrainer/api/security/JwtAuthenticationEntryPoint.kt new file mode 100644 index 0000000..2433af8 --- /dev/null +++ b/src/main/kotlin/com/aitrainer/api/security/JwtAuthenticationEntryPoint.kt @@ -0,0 +1,23 @@ +package com.aitrainer.api.security + +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.stereotype.Component +import java.io.IOException +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import java.io.Serializable +import org.springframework.security.core.AuthenticationException + + +@Component +class JwtAuthenticationEntryPoint : AuthenticationEntryPoint, Serializable { + @Throws(IOException::class) + override fun commence(request: HttpServletRequest?, response: HttpServletResponse, + authException: AuthenticationException?) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized") + } + + companion object { + private const val serialVersionUID = -7858869558953243875L + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/aitrainer/api/security/JwtRequest.kt b/src/main/kotlin/com/aitrainer/api/security/JwtRequest.kt new file mode 100644 index 0000000..58cbb19 --- /dev/null +++ b/src/main/kotlin/com/aitrainer/api/security/JwtRequest.kt @@ -0,0 +1,19 @@ +package com.aitrainer.api.security + +import java.io.Serializable + + +class JwtRequest : Serializable { + var username: String? = null + var password: String? = null + + //default constructor for JSON Parsing + constructor(username: String?, password: String?) { + this.username = username + this.password = password + } + + companion object { + private const val serialVersionUID = 5926468583005150707L + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/aitrainer/api/security/JwtRequestFilter.kt b/src/main/kotlin/com/aitrainer/api/security/JwtRequestFilter.kt new file mode 100644 index 0000000..35cdd56 --- /dev/null +++ b/src/main/kotlin/com/aitrainer/api/security/JwtRequestFilter.kt @@ -0,0 +1,77 @@ +package com.aitrainer.api.security + +import com.aitrainer.api.service.UserDetailsServiceImpl +import io.jsonwebtoken.ExpiredJwtException +import org.springframework.beans.factory.annotation.Autowired +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 +import java.io.IOException +import javax.servlet.FilterChain +import javax.servlet.ServletException +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + + +@Component +class JwtRequestFilter : OncePerRequestFilter() { + @Autowired + private val jwtUserDetailsService: UserDetailsServiceImpl? = null + + @Autowired + private val jwtTokenUtil: JwtTokenUtil? = null + + //@Autowired + //private lateinit var authenticationController: JwtAuthenticationController + + @Throws(ServletException::class, IOException::class) + override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) { + val requestTokenHeader = request.getHeader("Authorization") + var username: String? = null + var jwtToken: String? = null + // JWT Token is in the form "Bearer token". Remove Bearer word and get only the Token + if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer")) { + jwtToken = requestTokenHeader.substring(7) + try { + username = jwtTokenUtil!!.getUsernameFromToken(jwtToken) + } catch (e: IllegalArgumentException) { + println("Unable to get JWT Token") + } catch (e: ExpiredJwtException) { + println("JWT Token has expired") + } + } else if (requestTokenHeader != null && requestTokenHeader.equals("1") ) { + logger.warn("Authenticate") + //val credentials: User = ObjectMapper().readValue(request.inputStream, User::class.java) + + } else { + logger.warn("JWT Token does not begin with Bearer String") + } + + //Once we get the token validate it. + if (username != null && SecurityContextHolder.getContext().authentication == null) { + val userDetails: UserDetails = jwtUserDetailsService!!.loadUserByUsername(username) + + // if token is valid configure Spring Security to manually set authentication + if (jwtTokenUtil!!.validateToken(jwtToken!!, userDetails)) { + val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.authorities) + usernamePasswordAuthenticationToken.details = WebAuthenticationDetailsSource().buildDetails(request) + // After setting the Authentication in the context, we specify + // that the current user is authenticated. So it passes the Spring Security Configurations successfully. + SecurityContextHolder.getContext().authentication = usernamePasswordAuthenticationToken + } + } + chain.doFilter(request, response) + } + + /*private fun readUserCredentials(request: HttpServletRequest): UserCredentials? { + return try { + ObjectMapper().readValue(request.inputStream, UserCredentials::class.java) + } catch (ioe: IOException) { + throw BadCredentialsException("Invalid request", ioe) + } + }*/ +} diff --git a/src/main/kotlin/com/aitrainer/api/security/JwtResponse.kt b/src/main/kotlin/com/aitrainer/api/security/JwtResponse.kt new file mode 100644 index 0000000..d7227fa --- /dev/null +++ b/src/main/kotlin/com/aitrainer/api/security/JwtResponse.kt @@ -0,0 +1,12 @@ +package com.aitrainer.api.security + +import java.io.Serializable + + +class JwtResponse(val token: String) : Serializable { + + companion object { + private const val serialVersionUID = -8091879091924046844L + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/aitrainer/api/security/JwtSecurityConfig.kt b/src/main/kotlin/com/aitrainer/api/security/JwtSecurityConfig.kt new file mode 100644 index 0000000..b299268 --- /dev/null +++ b/src/main/kotlin/com/aitrainer/api/security/JwtSecurityConfig.kt @@ -0,0 +1,65 @@ +package com.aitrainer.api.security + +import com.aitrainer.api.service.ServiceBeans +import com.aitrainer.api.service.UserDetailsServiceImpl +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +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.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + + +@Configuration +@EnableWebSecurity +class JwtSecurityConfig : WebSecurityConfigurerAdapter() { + @Autowired + private val jwtAuthenticationEntryPoint: JwtAuthenticationEntryPoint? = null + + @Autowired + private val jwtUserDetailsService: UserDetailsServiceImpl? = null + + @Autowired + private val jwtRequestFilter: JwtRequestFilter? = null + + @Autowired + private val serviceBeans: ServiceBeans? = null + + override fun configure(auth: AuthenticationManagerBuilder?) { + auth!!.userDetailsService(jwtUserDetailsService).passwordEncoder(serviceBeans!!.passwordEncoder()) + } + + @Bean + @Throws(Exception::class) + override fun authenticationManagerBean(): AuthenticationManager { + return super.authenticationManagerBean() + } + + @Throws(Exception::class) + override fun configure(httpSecurity: HttpSecurity) { + + // We don't need CSRF for this example + httpSecurity. + csrf().disable(). + // dont authenticate this particular request + authorizeRequests().antMatchers("/api/authenticate").permitAll(). + // all other requests need to be authenticated + anyRequest().authenticated().and(). + // make sure we use stateless session; session won't be used to + // store user's state. + exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and(). + // Add a filter to validate the tokens with every request + //addFilterAt(JwtAuthenticationFilter(authenticationManagerBean()), UsernamePasswordAuthenticationFilter::class.java). + addFilterAfter(jwtRequestFilter, UsernamePasswordAuthenticationFilter::class.java). + sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + + + + } + + +} diff --git a/src/main/kotlin/com/aitrainer/api/security/JwtTokenUtil.kt b/src/main/kotlin/com/aitrainer/api/security/JwtTokenUtil.kt new file mode 100644 index 0000000..8581cf3 --- /dev/null +++ b/src/main/kotlin/com/aitrainer/api/security/JwtTokenUtil.kt @@ -0,0 +1,67 @@ +package com.aitrainer.api.security + +import org.springframework.beans.factory.annotation.Value +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.stereotype.Component +import java.util.* +import java.io.Serializable + +import io.jsonwebtoken.Claims +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm + + +@Component +class JwtTokenUtil : Serializable { + @Value("\${jwt.secret}") + private val secret: String? = null + fun getUsernameFromToken(token: String?): String { + return getClaimFromToken(token, Claims::getSubject) + } + + fun getIssuedAtDateFromToken(token: String?): Date { + return getClaimFromToken(token, Claims::getIssuedAt) + } + + fun getExpirationDateFromToken(token: String?): Date { + return getClaimFromToken(token, Claims::getExpiration) + } + + fun getClaimFromToken( token: String?, claimsResolver: ( Claims.()-> T ) ): T { + val claims: Claims = getAllClaimsFromToken(token) + return claims.claimsResolver() + } + + private fun getAllClaimsFromToken(token: String?): Claims { + return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).body + } + + private fun isTokenExpired(token: String): Boolean { + val expiration: Date = getExpirationDateFromToken(token) + return expiration.before(Date()) + } + + fun generateToken(userDetails: UserDetails): String { + val claims: Map = HashMap() + return doGenerateToken(claims, userDetails.username) + } + + private fun doGenerateToken(claims: Map, subject: String): String { + return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(Date(System.currentTimeMillis())) + .setExpiration(Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000)).signWith(SignatureAlgorithm.HS512, secret).compact() + } + + fun canTokenBeRefreshed(token: String): Boolean { + return !isTokenExpired(token) + } + + fun validateToken(token: String, userDetails: UserDetails): Boolean { + val username = getUsernameFromToken(token) + return username == userDetails.username && !isTokenExpired(token) + } + + companion object { + private const val serialVersionUID = -2550185165626007488L + const val JWT_TOKEN_VALIDITY = 5 * 60 * 60.toLong() + } +} diff --git a/src/main/kotlin/com/aitrainer/api/service/ServiceBeans.kt b/src/main/kotlin/com/aitrainer/api/service/ServiceBeans.kt new file mode 100644 index 0000000..8880030 --- /dev/null +++ b/src/main/kotlin/com/aitrainer/api/service/ServiceBeans.kt @@ -0,0 +1,19 @@ +package com.aitrainer.api.service + +import org.springframework.context.annotation.Bean +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Component + +/* + Commonly used Beans + */ + +@Component +class ServiceBeans { + + @Bean + fun passwordEncoder(): PasswordEncoder { + return BCryptPasswordEncoder() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/aitrainer/api/service/UserDetailsServiceImpl.kt b/src/main/kotlin/com/aitrainer/api/service/UserDetailsServiceImpl.kt new file mode 100644 index 0000000..a3482e4 --- /dev/null +++ b/src/main/kotlin/com/aitrainer/api/service/UserDetailsServiceImpl.kt @@ -0,0 +1,34 @@ +package com.aitrainer.api.service + +import com.aitrainer.api.model.Customer +import com.aitrainer.api.repository.CustomerRepository +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.core.GrantedAuthority +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.authority.SimpleGrantedAuthority +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import kotlin.collections.HashSet + +@Service +class UserDetailsServiceImpl: UserDetailsService { + + @Autowired + private lateinit var customerRepository: CustomerRepository + + @Override + @Transactional(readOnly = true) + override fun loadUserByUsername(username: String?): UserDetails { + val customer: Customer? = customerRepository.findByEmail(username) + + val grantedAuthorities = HashSet() + grantedAuthorities.add(SimpleGrantedAuthority("user")) + + if (customer != null) { + return User(customer.email, customer.password, grantedAuthorities) + } else {throw Exception("User does not exist")} + + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b85926c..fa36823 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -16,4 +16,6 @@ logging.config=classpath:logback-spring.xml logging.file=logs # if the database structure has been changed, increment this version number -application.version=0.0.2 \ No newline at end of file +application.version=0.0.3 + +jwt.secret=aitrainer \ No newline at end of file diff --git a/src/test/kotlin/com/aitrainer/api/test/AuthenticationTest.kt b/src/test/kotlin/com/aitrainer/api/test/AuthenticationTest.kt new file mode 100644 index 0000000..83c0b35 --- /dev/null +++ b/src/test/kotlin/com/aitrainer/api/test/AuthenticationTest.kt @@ -0,0 +1,25 @@ +package com.aitrainer.api.test + +import com.aitrainer.api.security.JwtAuthenticationController +import com.aitrainer.api.security.JwtRequest +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import kotlin.test.assertEquals + +@SpringBootTest +class AuthenticationTest { + + @Autowired + private lateinit var authController: JwtAuthenticationController + @Test + fun testAuthentication() { + val response: ResponseEntity<*> + val jwtRequest = JwtRequest("bosi", "andio2009") + response = authController.generateAuthenticationToken(jwtRequest) + assertEquals(response.statusCode, HttpStatus.OK) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/aitrainer/api/test/CustomerTests.kt b/src/test/kotlin/com/aitrainer/api/test/CustomerTests.kt index d1c5a5a..86c6996 100644 --- a/src/test/kotlin/com/aitrainer/api/test/CustomerTests.kt +++ b/src/test/kotlin/com/aitrainer/api/test/CustomerTests.kt @@ -1,16 +1,32 @@ package com.aitrainer.api.test +import com.aitrainer.api.controller.CustomerController import com.aitrainer.api.model.Customer +import com.aitrainer.api.model.User import com.aitrainer.api.repository.CustomerRepository +import com.aitrainer.api.service.ServiceBeans +import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity import kotlin.test.assertEquals import kotlin.test.assertNotNull @SpringBootTest +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class CustomerTests { + @Autowired + private var serviceBean: ServiceBeans? = null + + @BeforeAll + fun init() { + if ( serviceBean == null ) { serviceBean = ServiceBeans() } + } + @Autowired private lateinit var customerRepository: CustomerRepository private var insertedId: Long? = null @@ -49,6 +65,58 @@ class CustomerTests { assertEquals( customer.firstname, "Tiborka") customerRepository.delete(updatedCustomer) - } + } + @Test + fun testRegistration() { + val json = "{\"username\":\"bosi@example.com\",\"password\":\"94385\"}" + val user: User = User().fromJson(json) + assertEquals(user.username, "bosi@example.com") + val customer = Customer() + with(customer) { + email = user.username + password = user.password + } + val customerController = CustomerController(customerRepository) + customerController.serviceBeans = serviceBean + var response: ResponseEntity<*> = customerController.registration(json) + val newCustomer: Customer? = response.body as Customer + assertEquals(response.statusCode, HttpStatus.OK) + + val json2 = "{\"username\":\"bosi@example.com\",\"password\":\"934345\"}" + response = customerController.registration(json2) + assertEquals(response.statusCode, HttpStatus.BAD_REQUEST) + + if ( newCustomer != null) { + customerRepository.delete(newCustomer) + } + } + + @Test fun testLogin() { + val json = "{\"username\":\"bosi2@example.com\",\"password\":\"94333385\"}" + val user: User = User().fromJson(json) + val customer = Customer() + with(customer) { + email = user.username + password = user.password + } + val customerController = CustomerController(customerRepository) + customerController.serviceBeans = serviceBean + var response: ResponseEntity<*> = customerController.registration(json) + val newCustomer: Customer? = response.body as Customer + assertEquals(response.statusCode, HttpStatus.OK) + + response = customerController.login(json) + val loginedCustomer: Customer? = response.body as Customer + assertEquals(response.statusCode, HttpStatus.OK) + if ( loginedCustomer != null ) { + assertEquals(loginedCustomer.email, ("bosi2@example.com") ) + } else { + assert(true) + } + + if ( newCustomer != null) { + customerRepository.delete(newCustomer) + } + } } \ No newline at end of file