Skip to content

Commit

Permalink
chore: stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
h3nryc0ding committed Jan 30, 2024
1 parent 62e3ddc commit 3e0b9eb
Show file tree
Hide file tree
Showing 12 changed files with 101 additions and 176 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package opensource.h3nryc0ding.playground.config

import opensource.h3nryc0ding.playground.security.JWTHeadersExchangeMatcher
import opensource.h3nryc0ding.playground.security.ReactiveAuthenticationManager
import opensource.h3nryc0ding.playground.security.JWTTokenAuthenticationConverter
import opensource.h3nryc0ding.playground.security.ReactiveAuthenticationManager
import opensource.h3nryc0ding.playground.security.UnauthorizedAuthenticationEntryPoint
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
Expand All @@ -16,19 +16,14 @@ import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.AuthenticationWebFilter
import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers

@Configuration
@EnableReactiveMethodSecurity
@EnableWebFluxSecurity
class SecurityConfiguration {
companion object {
private val AUTH_WHITELIST =
arrayOf(
"/resources/**",
"/webjars/**",
"/authorize/**",
"/favicon.ico",
)
val GRAPHQL_WHITELIST = arrayOf("/graphql/**", "/graphiql/**")
}

@Bean
Expand All @@ -46,13 +41,12 @@ class SecurityConfiguration {
exceptionHandling {
authenticationEntryPoint = entryPoint
}

authorizeExchange {
// TODO
authorize("/graphql/**", permitAll)
authorize("/graphiql/**", permitAll)
authorize("/api/authenticate", permitAll)
authorize(pathMatchers(*GRAPHQL_WHITELIST), permitAll)
authorize(anyExchange, authenticated)
}

addFilterAt(
authFilter,
SecurityWebFiltersOrder.AUTHORIZATION,
Expand All @@ -62,9 +56,9 @@ class SecurityConfiguration {

@Bean
fun webFilter(
reactiveAuthenticationManager: ReactiveAuthenticationManager,
exchangeMatcher: JWTHeadersExchangeMatcher,
authConverter: JWTTokenAuthenticationConverter,
reactiveAuthenticationManager: ReactiveAuthenticationManager,
exchangeMatcher: JWTHeadersExchangeMatcher,
authConverter: JWTTokenAuthenticationConverter,
): AuthenticationWebFilter {
return AuthenticationWebFilter(reactiveAuthenticationManager).apply {
setRequiresAuthenticationMatcher(exchangeMatcher)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,21 @@ class JWTTokenAuthenticationConverter(
) : ServerAuthenticationConverter {
override fun convert(serverWebExchange: ServerWebExchange?): Mono<Authentication> {
return Mono.justOrEmpty(serverWebExchange)
.log()
.flatMap { exchange ->
val token = exchange.request.headers[HttpHeaders.AUTHORIZATION]?.firstOrNull()
if (token != null && token.length > BEARER.length) {
val authToken = token.substring(BEARER.length)
if (authToken.isNotBlank()) {
return@flatMap try {
Mono.just(tokenProvider.getAuthentication(authToken))
// TODO: handle errors better
} catch (_: Exception) {
Mono.empty()
}
.log()
.flatMap { exchange ->
val token = exchange.request.headers[HttpHeaders.AUTHORIZATION]?.firstOrNull()
if (token != null && token.length > BEARER.length) {
val authToken = token.substring(BEARER.length)
if (authToken.isNotBlank()) {
return@flatMap try {
Mono.just(tokenProvider.getAuthentication(authToken))
// TODO: handle errors better
} catch (_: Exception) {
Mono.empty()
}
}
return@flatMap Mono.empty()
}
return@flatMap Mono.empty()
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import reactor.kotlin.core.publisher.switchIfEmpty

@Component
class ReactiveAuthenticationManager(
private val userDetailsService: ReactiveUserDetailsService,
private val passwordEncoder: PasswordEncoder,
private val userDetailsService: ReactiveUserDetailsService,
private val passwordEncoder: PasswordEncoder,
) : ReactiveAuthenticationManager {
@Throws(BadCredentialsException::class)
override fun authenticate(authentication: Authentication): Mono<Authentication> {
Expand All @@ -22,15 +22,15 @@ class ReactiveAuthenticationManager(
}

val authToken =
authentication as? UsernamePasswordAuthenticationToken
?: return Mono.error(BadCredentialsException("Invalid Credentials"))
authentication as? UsernamePasswordAuthenticationToken
?: return Mono.error(BadCredentialsException("Invalid Credentials"))

return userDetailsService.findByUsername(authToken.name)
// TODO: should we publishOn(Schedulers.parallel())?
.filter { userDetails -> passwordEncoder.matches(authToken.credentials as String, userDetails.password) }
.switchIfEmpty { Mono.error(BadCredentialsException("Invalid Credentials")) }
.map { userDetails ->
UsernamePasswordAuthenticationToken(userDetails.username, userDetails.password, userDetails.authorities)
}
// TODO: should we publishOn(Schedulers.parallel())?
.filter { userDetails -> passwordEncoder.matches(authToken.credentials as String, userDetails.password) }
.switchIfEmpty { Mono.error(BadCredentialsException("Invalid Credentials")) }
.map { userDetails ->
UsernamePasswordAuthenticationToken(userDetails.username, userDetails.password, userDetails.authorities)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package opensource.h3nryc0ding.playground.security

import opensource.h3nryc0ding.playground.user.UserRepository
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.core.userdetails.ReactiveUserDetailsService
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.switchIfEmpty

@Service
class ReactiveUserDetailsService(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
@file:Suppress("LoggingPlaceholderCountMatchesArgumentCount")

package opensource.h3nryc0ding.playground.security

import io.jsonwebtoken.ExpiredJwtException
Expand Down Expand Up @@ -29,55 +27,54 @@ object TokenProvider {

fun createToken(authentication: Authentication): String {
val authorities =
authentication.authorities
.joinToString(",") { it.authority }
authentication.authorities
.joinToString(",") { it.authority }

val now = Date().time
val validity = Date(now + TOKEN_VALIDITY)

log.info("Creating token for user ${authentication.name}")

return Jwts.builder()
.issuer(ISSUER)
.subject(authentication.name)
.claim(AUTHORITIES_KEY, authorities)
.expiration(validity)
.signWith(KEY)
.compact()
.issuer(ISSUER)
.subject(authentication.name)
.claim(AUTHORITIES_KEY, authorities)
.expiration(validity)
.signWith(KEY)
.compact()
}

fun getAuthentication(token: String): Authentication {
if (token.isBlank() || !validateToken(token)) {
throw BadCredentialsException("Invalid token")
}
val claims =
Jwts.parser()
.requireIssuer(ISSUER)
.verifyWith(KEY)
.build()
.parseSignedClaims(token)
.payload
Jwts.parser()
.requireIssuer(ISSUER)
.verifyWith(KEY)
.build()
.parseSignedClaims(token)
.payload

val authorities =
claims[AUTHORITIES_KEY]
.toString()
.split(",")
.filter { it.isNotEmpty() }
.map { SimpleGrantedAuthority(it) }
claims[AUTHORITIES_KEY]
.toString()
.split(",")
.filter { it.isNotEmpty() }
.map { SimpleGrantedAuthority(it) }

val principal = User(claims.subject, "", authorities)
log.info("Creating authentication for user ${principal.username}")
return UsernamePasswordAuthenticationToken(principal, token, authorities)

}

private fun validateToken(authToken: String): Boolean {
try {
Jwts.parser()
.requireIssuer(ISSUER)
.verifyWith(KEY)
.build()
.parseSignedClaims(authToken)
.requireIssuer(ISSUER)
.verifyWith(KEY)
.build()
.parseSignedClaims(authToken)

return true
} catch (e: SignatureException) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,42 @@ package opensource.h3nryc0ding.playground.user

import com.netflix.graphql.dgs.DgsComponent
import com.netflix.graphql.dgs.DgsMutation
import com.netflix.graphql.dgs.DgsQuery
import com.netflix.graphql.dgs.InputArgument
import com.netflix.graphql.dgs.DgsQuery
import com.netflix.graphql.dgs.DgsDataFetchingEnvironment
import opensource.h3nryc0ding.playground.generated.types.AuthenticationInput
import opensource.h3nryc0ding.playground.security.ReactiveAuthenticationManager
import opensource.h3nryc0ding.playground.security.TokenProvider
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.ReactiveSecurityContextHolder
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
import java.security.Principal
import opensource.h3nryc0ding.playground.generated.types.User as UserDTO
import org.springframework.security.core.userdetails.User


@DgsComponent
class UserDataFetcher(
private val tokenProvider: TokenProvider,
private val authenticationManager: ReactiveAuthenticationManager,
private val tokenProvider: TokenProvider,
private val authenticationManager: ReactiveAuthenticationManager,
) {
@DgsMutation
fun authenticate(
@InputArgument input: AuthenticationInput,
@InputArgument input: AuthenticationInput,
): Mono<String> {
val authenticationToken =
UsernamePasswordAuthenticationToken(input.username, input.password)
UsernamePasswordAuthenticationToken(input.username, input.password)

return authenticationManager.authenticate(authenticationToken)
.map { authentication ->
ReactiveSecurityContextHolder.withAuthentication(authentication)
tokenProvider.createToken(authentication)
}
.map { authentication ->
ReactiveSecurityContextHolder.withAuthentication(authentication)
tokenProvider.createToken(authentication)
}
}

@DgsQuery
@PreAuthorize("isAuthenticated()")
fun currentUser(serverWebExchange: ServerWebExchange): Mono<UserDTO> {
return serverWebExchange.getPrincipal<Principal>()
.cast(UserDTO::class.java)
fun currentUser(dfe: DgsDataFetchingEnvironment): Mono<User> {
return ReactiveSecurityContextHolder.getContext()
.map { it.authentication.principal as User }
}
}
}
1 change: 0 additions & 1 deletion backend/src/main/resources/schema/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ type Subscription {

# BEGIN: User
type User {
id: ID!
username: String!
authorities: [String!]!
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package opensource.h3nryc0ding.playground.security

import opensource.h3nryc0ding.playground.security.JWTHeadersExchangeMatcher
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
Expand Down
Loading

0 comments on commit 3e0b9eb

Please sign in to comment.