Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2973573
completed hello world exercise
mohammedsheshtar Apr 8, 2025
e1418dc
built a backend microservice that accepts and displays orders via our…
mohammedsheshtar Apr 8, 2025
8ceaeb0
Added annother variable in my data class to account for what time was…
mohammedsheshtar Apr 8, 2025
f98ce60
Replaced H2 with PostgreSQL for persistent order storage
mohammedsheshtar Apr 9, 2025
0e28866
Added some comments for documentation
mohammedsheshtar Apr 9, 2025
a2de943
finished the bonus task where I was able to create a separate databas…
mohammedsheshtar Apr 12, 2025
0e727b5
separated business logic and controller endpoints. Added authenticati…
mohammedsheshtar Apr 17, 2025
26d3170
Implement JWT authentication to secure order submissions and profile …
mohammedsheshtar Apr 21, 2025
0ef3254
completed the bonus task for user authentication for fun :)
mohammedsheshtar Apr 22, 2025
10e35cc
tests now saves into the H2 databse. Added new tests for profiles and…
mohammedsheshtar Apr 23, 2025
2319b0b
Added more tests to ensure the validation checkpoints that can be fou…
mohammedsheshtar Apr 28, 2025
3f62688
Implmented a logging filter layer into the design
mohammedsheshtar Apr 29, 2025
73d59a8
Implmented a logging filter layer into the design
mohammedsheshtar Apr 29, 2025
a60e36a
Added Hazelcast caching for menu retrieval and invalidation logic to …
mohammedsheshtar Apr 29, 2025
145290c
Implemented API documentation through Swagger and added a JSON file t…
mohammedsheshtar Apr 29, 2025
d76072a
Completed configuration task and updated the Swagger machine readable…
mohammedsheshtar Apr 29, 2025
a12de34
Converted the project from a monolithic server to a microservices ser…
mohammedsheshtar May 2, 2025
6e8a04d
minor changes and included the correct Swagger JSON files for each mi…
mohammedsheshtar May 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions authentication/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.coded.spring</groupId>
<artifactId>Kotlin.SpringbootV2</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>authentication</artifactId>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.code.style>official</kotlin.code.style>
<kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
</properties>

</project>
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.coded.spring.ordering
package com.coded.spring.authentication


import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class Application
class AuthenticationApplication

fun main(args: Array<String>) {
runApplication<Application>(*args)
}
runApplication<AuthenticationApplication>(*args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.coded.spring.authentication

import com.coded.spring.authentication.jwt.JwtService
import com.coded.spring.authentication.users.UserService
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
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 java.security.Principal


@Tag(name="AuthenticationAPI")
@RestController
@RequestMapping("/authentication")
class AuthenticationController(
private val authenticationManager: AuthenticationManager,
private val userDetailsService: UserDetailsService,
private val jwtService: JwtService,
private val userService: UserService
) {
@Tag(name = "AuthenticationAPI")
@PostMapping("/login")
fun login(@RequestBody authRequest: AuthenticationRequest): AuthenticationResponse {
val authToken = UsernamePasswordAuthenticationToken(authRequest.username, authRequest.password)
val authentication = authenticationManager.authenticate(authToken)

if (authentication.isAuthenticated) {
val userDetails = userDetailsService.loadUserByUsername(authRequest.username)
val token = jwtService.generateToken(userDetails.username)
return AuthenticationResponse (token)
} else {
throw UsernameNotFoundException("Invalid user request!")
}
}

@PostMapping("/check-token")
fun checkToken(
principal: Principal
): CheckTokenResponse {
return CheckTokenResponse(
userId = userService.findByUsername(principal.name)
)
}
}


data class AuthenticationRequest(
val username: String,
val password: String
)

data class AuthenticationResponse(
val token: String
)

data class CheckTokenResponse(
val userId: Long
)

data class RegisterRequest(
val username: String,
val password: String
)

data class RegisterFailureResponse(
val error: AddUserError
)

enum class AddUserError {
INVALID_USERNAME, TOO_SHORT_PASSWORD, MAX_ACCOUNT_LIMIT_REACHED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.coded.spring.authentication


import com.coded.spring.authentication.users.UserEntity
import com.coded.spring.authentication.users.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

@Service
class CustomUserDetailsService(
private val userRepository: UserRepository
) : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
val user : UserEntity = userRepository.findByUsername(username) ?:
throw UsernameNotFoundException("User not found...")

return User.builder()
.username(user.username)
.password(user.password)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.coded.spring.authentication

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.util.ContentCachingRequestWrapper
import org.springframework.web.util.ContentCachingResponseWrapper

@Component
class LoggingFilter : OncePerRequestFilter() {

private val logger = LoggerFactory.getLogger(LoggingFilter::class.java)
private val objectMapper = ObjectMapper().apply {
enable(SerializationFeature.INDENT_OUTPUT)
}

private val RESET = "\u001B[0m"
private val GREEN = "\u001B[32m"
private val YELLOW = "\u001B[33m"
private val RED = "\u001B[31m"

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val cachedRequest = ContentCachingRequestWrapper(request)
val cachedResponse = ContentCachingResponseWrapper(response)

filterChain.doFilter(cachedRequest, cachedResponse)

logRequest(cachedRequest)
logResponse(cachedResponse)

cachedResponse.copyBodyToResponse()
}

private fun logRequest(request: ContentCachingRequestWrapper) {
val requestBody = request.contentAsByteArray.toString(Charsets.UTF_8).trim()

logger.info(
"""
|[*] Incoming Request
|Method: ${request.method}
|URI: ${request.requestURI}
|Body: ${formatJsonIfPossible(requestBody)}
|------------------------------------------------------------------------------------------------
""".trimMargin()
)
}

private fun logResponse(response: ContentCachingResponseWrapper) {
val responseBody = response.contentAsByteArray.toString(Charsets.UTF_8).trim()
val color = getColorForStatus(response.status)

logger.info(
"""
|[*] Outgoing Response
|Status: $color${response.status}$RESET
|Body: ${formatJsonIfPossible(responseBody)}
|===============================================================================================
""".trimMargin()
)
}

private fun formatJsonIfPossible(content: String): String {
return try {
if (content.isBlank()) {
"(empty body)"
} else {
val jsonNode = objectMapper.readTree(content)
objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode)
}
} catch (ex: Exception) {
content
}
}

private fun getColorForStatus(status: Int): String {
return when {
status in 200..299 -> GREEN
status in 400..499 -> YELLOW
status >= 500 -> RED
else -> RESET
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:9002","description":"Generated server url"}],"paths":{"/register":{"post":{"tags":["UserAPI"],"operationId":"registerUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/profile":{"post":{"tags":["ProfileAPI"],"operationId":"addProfile","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestProfileDTO"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/authentication/login":{"post":{"tags":["AuthenticationAPI"],"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthenticationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthenticationResponse"}}}}}}},"/authentication/check-token":{"post":{"tags":["AuthenticationAPI"],"operationId":"checkToken","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CheckTokenResponse"}}}}}}}},"components":{"schemas":{"CreateUserRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"RequestProfileDTO":{"required":["firstName","lastName","phoneNumber"],"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"}}},"AuthenticationRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"AuthenticationResponse":{"required":["token"],"type":"object","properties":{"token":{"type":"string"}}},"CheckTokenResponse":{"required":["userId"],"type":"object","properties":{"userId":{"type":"integer","format":"int64"}}}}}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.coded.spring.authentication

import com.coded.spring.authentication.jwt.JwtAuthenticationFilter
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.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.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter



@Configuration
@EnableWebSecurity
class SecurityConfig(
private val userDetailsService: CustomUserDetailsService,
private val jwtAuthFilter: JwtAuthenticationFilter,
) {

@Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()

@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http.csrf { it.disable() }
.authorizeHttpRequests {
it.requestMatchers(
"/register",
"/authentication/**",
"api-docs",
"/hello").permitAll() // public route
.anyRequest().authenticated()
}
.sessionManagement {
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter::class.java)

return http.build()
}

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

@Bean
fun authenticationProvider(): AuthenticationProvider {
val provider = DaoAuthenticationProvider()
provider.setUserDetailsService(userDetailsService)
provider.setPasswordEncoder(passwordEncoder())
return provider
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.coded.spring.authentication.jwt


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.UserDetailsService
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter

@Component
class JwtAuthenticationFilter(
private val jwtService: JwtService,
private val userDetailsService: UserDetailsService
) : OncePerRequestFilter() {

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val authHeader = request.getHeader("Authorization")
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response)
return
}

val token = authHeader.substring(7)
val username = jwtService.extractUsername(token)

if (SecurityContextHolder.getContext().authentication == null) {
if (jwtService.isTokenValid(token, username)) {
val userDetails = userDetailsService.loadUserByUsername(username)
val authToken = UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.authorities
)
authToken.details = WebAuthenticationDetailsSource().buildDetails(request)
SecurityContextHolder.getContext().authentication = authToken
}
}

filterChain.doFilter(request, response)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.coded.spring.authentication.jwt

import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.security.Keys
import org.springframework.stereotype.Component
import java.util.*
import javax.crypto.SecretKey

@Component
class JwtService {

private val secretKey: SecretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256)
private val expirationMs: Long = 1000 * 60 * 60

fun generateToken(username: String): String {
val now = Date()
val expiry = Date(now.time + expirationMs)

return Jwts.builder()
.setSubject(username)
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(secretKey)
.compact()
}

fun extractUsername(token: String): String =
Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.body
.subject

fun isTokenValid(token: String, username: String): Boolean {
return try {
extractUsername(token) == username
} catch (e: Exception) {
false
}
}
}
Loading