Initial commit

This commit is contained in:
jeobeardy committed 2024-08-16 10:50:57 +02:00
commit 2550140811
94 files changed
+10844

No files matched your search

@@ -0,0 +1,13 @@
package at.eisibaer.jbear2
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication
@SpringBootApplication
@ConfigurationPropertiesScan(basePackages = ["at.eisibaer.jbear2.config"])
class JeobeardyApplication
fun main(args: Array<String>) {
runApplication<JeobeardyApplication>(*args)
}
@@ -0,0 +1,12 @@
package at.eisibaer.jbear2.config
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("application")
data class ApplicationProperties(
val corsAllowedOrigins: List<String>,
val corsAllowedMethods: List<String>,
val jwtCookieName: String,
val jwtExpirationMs: Long,
val jwtSecret: String,
)
@@ -0,0 +1,51 @@
package at.eisibaer.jbear2.config
import jakarta.servlet.http.HttpServletRequest
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.Resource
import org.springframework.lang.Nullable
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import org.springframework.web.servlet.resource.PathResourceResolver
import org.springframework.web.servlet.resource.ResourceResolverChain
@Configuration
class SpringConfiguration(
private val applicationProperties: ApplicationProperties
) : WebMvcConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
this.serveDirectory(registry, "/", "classpath:/static/")
}
private fun serveDirectory(registry: ResourceHandlerRegistry, endpoint: String, location: String){
val endpointPatterns: Array<String>;
if (endpoint.endsWith("/") ){
endpointPatterns = arrayOf(endpoint.substring(0, endpoint.length - 1), endpoint, "$endpoint**")
} else {
endpointPatterns = arrayOf(endpoint, "$endpoint/", "$endpoint/**")
}
registry
.addResourceHandler(*endpointPatterns)
.addResourceLocations( if( location.endsWith("/") ) location else "$location/")
.resourceChain(true)
.addResolver(object : PathResourceResolver(){
override fun resolveResource( @Nullable request: HttpServletRequest?, requestPath: String, locations: MutableList<out Resource>, chain: ResourceResolverChain ): Resource? {
val resource: Resource? = super.resolveResource(request, requestPath, locations, chain);
if( resource != null ){
return resource;
}
return super.resolveResource(request, "/index.html", locations, chain);
}
})
}
override fun addCorsMappings(registry: CorsRegistry) {
registry
.addMapping("/api/**")
.allowedOrigins(*applicationProperties.corsAllowedOrigins.map { it }.toTypedArray())
.allowedMethods(*applicationProperties.corsAllowedMethods.map { it }.toTypedArray())
}
}
@@ -0,0 +1,21 @@
package at.eisibaer.jbear2.config
import org.springframework.context.annotation.Configuration
import org.springframework.messaging.simp.config.MessageBrokerRegistry
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer
@Configuration
@EnableWebSocketMessageBroker
class WebSocketConfig : WebSocketMessageBrokerConfigurer {
override fun configureMessageBroker(registry: MessageBrokerRegistry) {
registry.enableSimpleBroker("/game");
registry.setApplicationDestinationPrefixes("/app");
}
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/websocket")
}
}
@@ -0,0 +1,6 @@
package at.eisibaer.jbear2.dto.auth
data class LoginDto(
val username: String,
val password: String,
)
@@ -0,0 +1,6 @@
package at.eisibaer.jbear2.dto.auth
data class LoginResponseDto(
val username: String,
val profilePicture: String,
)
@@ -0,0 +1,11 @@
package at.eisibaer.jbear2.dto.message
import lombok.AllArgsConstructor
@AllArgsConstructor
data class GenericMessage (
var name: String? = "",
var content: String? = "",
)
@@ -0,0 +1,87 @@
package at.eisibaer.jbear2.endpoint
import at.eisibaer.jbear2.dto.auth.LoginDto
import at.eisibaer.jbear2.dto.auth.LoginResponseDto
import at.eisibaer.jbear2.model.Board
import at.eisibaer.jbear2.model.User
import at.eisibaer.jbear2.repository.UserRepository
import at.eisibaer.jbear2.security.jwt.JwtUtils
import at.eisibaer.jbear2.security.userdetail.UserDetailsImpl
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders
import org.springframework.http.ResponseCookie
import org.springframework.http.ResponseEntity
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
@Controller
@RequestMapping("/api/auth")
class AuthEndpoint(
val authenticationManager: AuthenticationManager,
val userRepository: UserRepository,
val encoder: PasswordEncoder,
val jwtUtils: JwtUtils,
) {
private val log: Logger = LoggerFactory.getLogger(AuthEndpoint::class.java);
val strResponseSuccess: String = "Sending back success response";
@PostMapping("/signup")
fun signupUser(@RequestBody loginDto: LoginDto): ResponseEntity<String>{
log.info("Endpoint singupUser called");
log.debug("signup Request with username: {}", loginDto.username);
if( userRepository.existsByUsername(loginDto.username)){
log.info("Username was already taken");
ResponseEntity.badRequest().body("Username already taken");
}
val user = User(loginDto.username, encoder.encode( loginDto.password), ArrayList(), null, null );
userRepository.save(user);
log.info(strResponseSuccess);
return ResponseEntity.ok().body("User registered successfully");
}
@PostMapping("/login")
fun loginUser(@RequestBody loginDto: LoginDto): ResponseEntity<LoginResponseDto>{
log.info("Endpoint loginUser called");
log.debug("login Request with username: {}", loginDto.username);
val authentication = authenticationManager.authenticate(
UsernamePasswordAuthenticationToken(
loginDto.username,
loginDto.password
)
)
SecurityContextHolder.getContext().authentication = authentication;
val userDetails: UserDetailsImpl = authentication.principal as UserDetailsImpl;
val jwtCookie = jwtUtils.generateJwtCookie(userDetails);
log.info(strResponseSuccess);
return ResponseEntity.ok()
.header( HttpHeaders.SET_COOKIE, jwtCookie.toString() )
.body( LoginResponseDto(userDetails.username, userDetails.getProfilePictureFilename()))
}
@PostMapping("signout")
fun logoutUser(): ResponseEntity<String>{
log.info("Endpoint logoutUser called");
val cookie: ResponseCookie = jwtUtils.getCleanJwtCookie();
log.info(strResponseSuccess);
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body("Logged out");
}
}
@@ -0,0 +1,9 @@
package at.eisibaer.jbear2.endpoint
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
@Controller
@RequestMapping("/api/board")
class BoardEndpoint {
}
@@ -0,0 +1,19 @@
package at.eisibaer.jbear2.endpoint
import org.springframework.messaging.handler.annotation.MessageMapping
import org.springframework.messaging.handler.annotation.SendTo
import at.eisibaer.jbear2.dto.message.GenericMessage
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/game")
class GameEndpoint {
@MessageMapping("/player/join")
@SendTo("/player/joined")
fun playerJoining(playerJoiningMessage: GenericMessage): GenericMessage{
return playerJoiningMessage.copy(playerJoiningMessage.name, "Joined");
}
}
@@ -0,0 +1,23 @@
package at.eisibaer.jbear2.endpoint
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/user")
class UserEndpoint {
val log: Logger = LoggerFactory.getLogger(UserEndpoint::class.java);
@GetMapping("/test/{pathVar}")
fun testEndpoint(@PathVariable pathVar: String, @RequestParam param1: String): ResponseEntity<String>{
log.info("test Endpoint!");
return ResponseEntity.ok(param1);
}
}
@@ -0,0 +1,25 @@
package at.eisibaer.jbear2.model
import jakarta.persistence.*
@Entity
@Table(name = "answers", indexes = [
Index(name = "fk_board_entry_to_answer", columnList = "fk_board_entry")
])
data class Answer(
@Column(name = "text", nullable = false, unique = false)
val text: String,
@OneToOne
@JoinColumn(name = "fk_image_file", referencedColumnName = "id")
val image: ImageFile?,
@OneToOne
@JoinColumn(name="fk_board_entry", referencedColumnName = "id")
val boardEntry: BoardEntry?,
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
val id: Long? = null,
)
@@ -0,0 +1,24 @@
package at.eisibaer.jbear2.model
import jakarta.persistence.*
@Entity
@Table(name = "boards", indexes = [
Index(name = "fk_owner_of_board", columnList = "fk_owned_by")
])
data class Board(
@OneToMany(mappedBy = "board")
val categories: List<Category>,
@Column(name = "board_name", nullable = false, unique = false)
val boardName: String,
@ManyToOne
@JoinColumn(name="fk_owned_by", referencedColumnName = "id")
val owner: User,
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
val id: Long? = null,
)
@@ -0,0 +1,27 @@
package at.eisibaer.jbear2.model
import jakarta.persistence.*
@Entity
@Table(name = "board_entries", indexes = [
Index(name = "fk_category_to_board_entry", columnList = "fk_category")
])
data class BoardEntry(
@Column(name = "name", nullable = false, unique = false)
val name: String,
@OneToMany(mappedBy = "boardEntry")
val questions: List<Question>,
@OneToOne
val answer: Answer,
@ManyToOne
@JoinColumn(name = "fk_category", referencedColumnName = "id")
val category: Category?,
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
val id: Long? = null,
)
@@ -0,0 +1,27 @@
package at.eisibaer.jbear2.model
import jakarta.persistence.*
@Entity
@Table(name = "categories", indexes = [
Index(name = "fk_category_to_board", columnList = "fk_board")
])
data class Category (
@Column(name = "name", nullable = false, unique = false)
val name: String,
@Column(name = "description", nullable = false, unique = false)
val description: String,
@OneToMany(mappedBy = "category")
val boardEntries: List<BoardEntry>,
@ManyToOne
@JoinColumn(name = "fk_board", referencedColumnName = "id")
val board: Board,
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
val id: Long? = null,
)
@@ -0,0 +1,51 @@
package at.eisibaer.jbear2.model
import jakarta.persistence.*
import org.springframework.data.annotation.CreatedDate
import java.time.Instant
@Entity
@Table(name = "games", indexes = [
Index(name = "fk_board_to_game", columnList = "fk_board"),
Index(name = "fk_host_to_game", columnList = "fk_host_user"),
Index(name = "fk_choosing_player_to_game", columnList = "fk_player_currently_choosing")
])
data class Game(
@Column(name = "invite_code", nullable = false, unique = true)
val inviteCode: String,
@ManyToOne
@JoinColumn(name = "fk_board", referencedColumnName = "id")
val board: Board,
@OneToOne
@JoinColumn(name="fk_host_user", referencedColumnName = "id")
val host: User,
@OneToMany(mappedBy = "currentGame")
val players: List<Player>,
@OneToOne
@JoinColumn(name="fk_player_currently_choosing", referencedColumnName = "id")
val currentlyChoosingPlayer: Player,
@ManyToMany
@JoinTable(
name = "answered_board_entries_in_games",
joinColumns = [JoinColumn(name = "fk_game")],
inverseJoinColumns = [JoinColumn(name = "fk_board_entry")]
)
val alreadyAnsweredEntries: List<BoardEntry>,
@Column(name = "accepting_answers", nullable = false, unique = false)
val acceptingAnswers: Boolean? = false,
@CreatedDate
@Column(name = "created_timestamp", nullable = false, unique = false)
val createdTimestamp: Instant,
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
val id: Long? = null,
)
@@ -0,0 +1,34 @@
package at.eisibaer.jbear2.model
import jakarta.persistence.*
import java.util.UUID
@Entity
@Table(name = "image_files", indexes = [
Index(name = "fk_owner_to_image_file", columnList = "fk_owned_by")
])
data class ImageFile (
@Column(name = "uuid", nullable = false, unique = true)
val uuid: UUID,
@Column(name = "filename", nullable = false, unique = false)
val filename: String,
@Column(name = "hash", nullable = false, unique = false)
val hash: String,
@ManyToOne
@JoinColumn(name="fk_owned_by", referencedColumnName = "id")
val owner: User,
@OneToOne
val question: Question?,
@OneToOne
val answer: Answer?,
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
val id: Long? = null,
)
@@ -0,0 +1,28 @@
package at.eisibaer.jbear2.model
import jakarta.persistence.*
@Entity
@Table(name = "players", indexes = [
Index(name = "fk_current_game_to_player", columnList = "fk_current_game"),
Index(name = "fk_name_to_player", columnList = "name"),
])
data class Player(
@Column(name = "name", nullable = false, unique = false)
val name: String,
@ManyToOne
@JoinColumn(name = "fk_current_game", referencedColumnName = "id")
val currentGame: Game,
@Column(name = "is_allowed_to_answer", nullable = false, unique = false)
val allowedToAnswer: Boolean? = false,
@Column(name = "points", nullable = false, unique = false)
val points: Int? = 0,
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
val id: Long? = null,
)
@@ -0,0 +1,31 @@
package at.eisibaer.jbear2.model
import jakarta.persistence.*
@Entity
@Table(name = "questions", indexes = [
Index(name = "fk_board_entry_to_question", columnList = "fk_board_entry"),
Index(name = "fk_image_file_to_question", columnList = "fk_image"),
Index(name = "fk_question_type_to_question", columnList = "fk_question_type"),
])
data class Question(
@Column(name = "text", nullable = false, unique = false)
val text: String,
@ManyToOne
@JoinColumn(name = "fk_question_type", referencedColumnName = "id")
val questionType: QuestionType,
@OneToOne
@JoinColumn(name = "fk_image", referencedColumnName = "id")
val image: ImageFile?,
@ManyToOne
@JoinColumn(name = "fk_board_entry", referencedColumnName = "id")
val boardEntry: BoardEntry?,
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
val id: Long? = null,
)
@@ -0,0 +1,21 @@
package at.eisibaer.jbear2.model
import jakarta.persistence.*
@Entity
@Table(name = "question_types")
data class QuestionType(
@Column(name = "title", nullable = false, unique = true)
val title: String,
@Column(name = "description", nullable = false, unique = true)
val description: String,
@Column(name = "active", nullable = false, unique = false)
val active: Boolean,
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
val id: Long? = null,
)
@@ -0,0 +1,28 @@
package at.eisibaer.jbear2.model
import jakarta.persistence.*
@Entity
@Table(name = "users", indexes = [
Index(name = "fk_profile_picture_to_user", columnList = "fk_profile_picture"),
Index(name = "username_to_user", columnList = "username"),
])
data class User(
@Column(name = "username", nullable = false, unique = true)
var username: String,
@Column(name = "password", nullable = false, unique = false)
var password: String,
@OneToMany(mappedBy = "owner")
val boards: List<Board>,
@OneToOne
@JoinColumn(name = "fk_profile_picture", referencedColumnName = "id")
var profilePicture: ImageFile?,
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
val id: Long? = null,
)
@@ -0,0 +1,14 @@
package at.eisibaer.jbear2.repository
import at.eisibaer.jbear2.model.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.util.*
@Repository
interface UserRepository : JpaRepository<User, Long> {
fun findUserByUsername(username: String): Optional<User>;
fun existsByUsername(username: String): Boolean;
}
@@ -0,0 +1,126 @@
package at.eisibaer.jbear2.security
import at.eisibaer.jbear2.security.jwt.AuthTokenFilter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletException
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
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.configurers.SessionManagementConfigurer
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
import org.springframework.security.web.csrf.*
import org.springframework.util.StringUtils
import org.springframework.web.filter.OncePerRequestFilter
import java.io.IOException
import java.util.function.Supplier
@Configuration
@EnableMethodSecurity
class SecurityConfiguration(
private val userDetailService: UserDetailsService,
private val unauthorizedHandler: AuthTokenFilter,
) {
final val log: Logger = LoggerFactory.getLogger(SecurityConfiguration::class.java);
@Bean
fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain {
return httpSecurity
.csrf { config ->
config
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(SpaCsrfTokenRequestHandler())
}
.addFilterAfter(CsrfCookieFilter(), BasicAuthenticationFilter::class.java)
.authorizeHttpRequests { config ->
config
.requestMatchers("/api/auth/*").permitAll()
.requestMatchers("/api/**").authenticated()
.requestMatchers("/profile").authenticated()
.requestMatchers("/**").permitAll()
}
.sessionManagement { config: SessionManagementConfigurer<HttpSecurity?> ->
config.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
.authenticationProvider(authenticationProvider())
.addFilterBefore(unauthorizedHandler, UsernamePasswordAuthenticationFilter::class.java)
.build()
}
class SpaCsrfTokenRequestHandler : CsrfTokenRequestAttributeHandler() {
private val delegate: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
override fun handle(request: HttpServletRequest, response: HttpServletResponse, csrfToken: Supplier<CsrfToken>) {
/*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body.
*/
delegate.handle(request, response, csrfToken)
}
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
/*
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
* to resolve the CsrfToken. This applies when a single-page application includes
* the header value automatically, which was obtained via a cookie containing the
* raw CsrfToken.
*/
return if (StringUtils.hasText(request.getHeader(csrfToken.headerName))) {
super.resolveCsrfTokenValue(request, csrfToken)
} else {
/*
* In all other cases (e.g. if the request contains a request parameter), use
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a
* hidden input.
*/
delegate.resolveCsrfTokenValue(request, csrfToken)
}
}
}
class CsrfCookieFilter : OncePerRequestFilter() {
@Throws(ServletException::class, IOException::class)
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
val csrfToken = request.getAttribute("_csrf") as CsrfToken
// Render the token value to a cookie by causing the deferred token to be loaded
csrfToken.token
filterChain.doFilter(request, response)
}
}
@Bean
fun passwordEncoder() : PasswordEncoder {
return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
}
@Bean
fun authenticationProvider(): DaoAuthenticationProvider{
val authProvider: DaoAuthenticationProvider = DaoAuthenticationProvider()
authProvider.setUserDetailsService(userDetailService)
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
fun authenticationManager(authConfig: AuthenticationConfiguration): AuthenticationManager{
return authConfig.authenticationManager;
}
}
@@ -0,0 +1,23 @@
package at.eisibaer.jbear2.security.jwt
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.AuthenticationEntryPoint
class AuthEntryPointJwt: AuthenticationEntryPoint {
val log: Logger = LoggerFactory.getLogger(AuthEntryPointJwt::class.java);
override fun commence(
request: HttpServletRequest?,
response: HttpServletResponse?,
authException: AuthenticationException?
) {
log.error("Unauthorized error: {}", authException?.message);
response?.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
}
}
@@ -0,0 +1,51 @@
package at.eisibaer.jbear2.security.jwt
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.Logger
import org.slf4j.LoggerFactory
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
import org.springframework.web.filter.OncePerRequestFilter
@Component
class AuthTokenFilter(
private var jwtUtils: JwtUtils?,
private var userDetailService: UserDetailsService?,
): OncePerRequestFilter() {
val log: Logger = LoggerFactory.getLogger(AuthTokenFilter::class.java);
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
try{
val jwt: String? = parseJwt(request);
if( jwt != null && jwtUtils!!.validateJwt(jwt) ){
val username: String = jwtUtils!!.getUserNameFromJwt(jwt);
val userDetails: UserDetails = userDetailService!!.loadUserByUsername( username );
val authentication = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities);
authentication.details = WebAuthenticationDetailsSource().buildDetails(request);
SecurityContextHolder.getContext().authentication = authentication;
}
} catch ( e: Exception ){
log.error("Cannot set user authentication", e);
}
filterChain.doFilter(request, response);
}
private fun parseJwt(request: HttpServletRequest): String?{
return jwtUtils!!.getJwtFromCookies(request);
}
}
@@ -0,0 +1,75 @@
package at.eisibaer.jbear2.security.jwt
import at.eisibaer.jbear2.config.ApplicationProperties
import at.eisibaer.jbear2.security.userdetail.UserDetailsImpl
import io.jsonwebtoken.JwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.UnsupportedJwtException
import io.jsonwebtoken.io.Decoders
import io.jsonwebtoken.security.Keys
import jakarta.servlet.http.HttpServletRequest
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.ResponseCookie
import org.springframework.stereotype.Component
import org.springframework.web.util.WebUtils
import java.util.Date
import javax.crypto.SecretKey
@Component
class JwtUtils(
private val applicationProperties: ApplicationProperties
) {
private val log: Logger = LoggerFactory.getLogger(JwtUtils::class.java);
fun getJwtFromCookies(request: HttpServletRequest): String?{
return WebUtils.getCookie(request, applicationProperties.jwtCookieName)?.value;
}
fun generateJwtCookie(userPrincipal: UserDetailsImpl): ResponseCookie{
val jwt: String = generateTokenFromUsername(userPrincipal.username)
return ResponseCookie.from(applicationProperties.jwtCookieName, jwt)
.path("/api")
.maxAge(applicationProperties.jwtExpirationMs/1000)
.httpOnly(true)
.build();
}
fun getCleanJwtCookie(): ResponseCookie {
return ResponseCookie.from(applicationProperties.jwtCookieName, "")
.path("/api")
.build();
}
fun getUserNameFromJwt(token: String): String{
return Jwts.parser().verifyWith(key()).build().parseSignedClaims(token).payload.subject;
}
fun key(): SecretKey{
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(applicationProperties.jwtSecret));
}
fun generateTokenFromUsername(username: String): String{
return Jwts.builder()
.subject(username)
.issuedAt(Date())
.expiration(Date(Date().time + applicationProperties.jwtExpirationMs))
.signWith(key())
.compact();
}
fun validateJwt(authToken: String): Boolean{
try {
Jwts.parser().verifyWith(key()).build().parseSignedClaims(authToken);
return true;
} catch (e: UnsupportedJwtException) {
log.error("UnsupportedJwtException {}", e.message);
} catch (e: IllegalArgumentException) {
log.error("IllegalArgumentException {}", e.message);
} catch (e: JwtException) {
log.error("JwtException {}", e.message);
}
return false;
}
}
@@ -0,0 +1,28 @@
package at.eisibaer.jbear2.security.userdetail
import at.eisibaer.jbear2.repository.UserRepository
import at.eisibaer.jbear2.model.User
import lombok.AllArgsConstructor
import lombok.NoArgsConstructor
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Component
@AllArgsConstructor
@NoArgsConstructor
@Component
class UserDetailServiceImpl(
val userRepository: UserRepository,
): UserDetailsService {
override fun loadUserByUsername(username: String?): UserDetailsImpl {
val user: User = userRepository.findUserByUsername( username ?: "" ).orElseThrow { UsernameNotFoundException("User not found by username \"$username\"") }
return UserDetailsImpl(
user.id!!,
user.username,
user.password,
user.profilePicture?.filename,
);
}
}
@@ -0,0 +1,67 @@
package at.eisibaer.jbear2.security.userdetail
import com.fasterxml.jackson.annotation.JsonIgnore
import lombok.Data
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import java.util.Collections
@Data
class UserDetailsImpl(
private val id: Long,
private val username: String,
@JsonIgnore
private val password: String,
private val profilePictureFilename: String?,
): UserDetails {
override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
return Collections.emptyList();
}
override fun getPassword(): String {
return password;
}
override fun getUsername(): String {
return username;
}
fun getProfilePictureFilename(): String {
return profilePictureFilename ?: "";
}
fun getId(): Long{
return id;
}
override fun isAccountNonExpired(): Boolean {
return true
}
override fun isAccountNonLocked(): Boolean {
return true
}
override fun isCredentialsNonExpired(): Boolean {
return true
}
override fun isEnabled(): Boolean {
return true
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as UserDetailsImpl
return id == other.id
}
override fun hashCode(): Int {
return id.hashCode()
}
}