Skip to content

Commit

Permalink
authentication + authorization backend
Browse files Browse the repository at this point in the history
  • Loading branch information
thoomasbro committed May 22, 2024
1 parent 3c4589b commit b692bef
Show file tree
Hide file tree
Showing 56 changed files with 1,114 additions and 80 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package fr.gouv.cacem.monitorenv.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component

@Component
@ConfigurationProperties(prefix = "monitorenv.oidc")
class OIDCProperties {
var enabled: Boolean? = false
var userinfoEndpoint: String? = null
var issuerUri: String? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package fr.gouv.cacem.monitorenv.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component

@Component
@ConfigurationProperties(prefix = "monitorenv.api.protected")
class ProtectedPathsAPIProperties {
var paths: List<String>? = listOf()
var superUserPaths: List<String>? = listOf()
var publicPaths: List<String>? = listOf()
var apiKey: String = ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package fr.gouv.cacem.monitorenv.config

import fr.gouv.cacem.monitorenv.infrastructure.api.endpoints.log.CustomAuthenticationEntryPoint
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.Customizer
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource

@Configuration
@EnableWebSecurity
class SecurityConfig(
val oidcProperties: OIDCProperties,
val authenticationEntryPoint: CustomAuthenticationEntryPoint,
) {
private val logger: Logger = LoggerFactory.getLogger(SecurityConfig::class.java)

@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf { it.disable() }
.authorizeHttpRequests { authorize ->
if (oidcProperties.enabled == null || oidcProperties.enabled == false) {
logger.warn(
"""
⚠️ WARNING ⚠️ - OIDC Authentication is NOT enabled.
""".trimIndent(),
)

authorize.requestMatchers("/**").permitAll()
} else {
logger.warn(
"""
✅ OIDC Authentication is enabled.
""".trimIndent(),
)

authorize.requestMatchers(
"/",
"/index.html",
"/*.js",
"/*.png",
"/*.svg",
"/static/**",
"/assets/**",
"/map-icons/**",
"/flags/**",
"/robots.txt",
"/favicon-32.ico",
"/asset-manifest.json",
"/swagger-ui/**",
// Used to redirect to the frontend SPA, see SpaController.kt
"/error",
"/api/**",
"/version",
).permitAll()
.anyRequest()
.authenticated()
}
}.oauth2ResourceServer {
oauth2ResourceServer ->
oauth2ResourceServer
.jwt(Customizer.withDefaults())
.authenticationEntryPoint(authenticationEntryPoint)
}

return http.build()
}

@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration().apply {
allowedOrigins = listOf("*")
allowedMethods = listOf("HEAD", "GET", "POST", "PUT", "DELETE", "OPTIONS")
allowedHeaders = listOf("Authorization", "Cache-Control", "Content-Type")
}

val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", configuration)

return source
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package fr.gouv.cacem.monitorenv.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component

@Component
@ConfigurationProperties(prefix = "monitorenv.api.super-user")
data class SuperUserAPIProperties(
var paths: List<String>? = listOf(),
)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package fr.gouv.cacem.monitorenv.domain.entities.authorization

data class UserAuthorization(
val hashedEmail: String,
val isSuperUser: Boolean,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package fr.gouv.cacem.monitorenv.domain.repositories

import fr.gouv.cacem.monitorenv.domain.entities.authorization.UserAuthorization

interface IUserAuthorizationRepository {
fun findByHashedEmail(hashedEmail: String): UserAuthorization
fun save(user: UserAuthorization)
fun delete(hashedEmail: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package fr.gouv.cacem.monitorenv.domain.use_cases.authorization

import fr.gouv.cacem.monitorenv.config.UseCase
import fr.gouv.cacem.monitorenv.domain.hash
import fr.gouv.cacem.monitorenv.domain.repositories.IUserAuthorizationRepository

@UseCase
class DeleteUser(
private val userAuthorizationRepository: IUserAuthorizationRepository,
) {
fun execute(email: String) {
val hashedEmail = hash(email)

userAuthorizationRepository.delete(hashedEmail)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package fr.gouv.cacem.monitorenv.domain.use_cases.authorization

import fr.gouv.cacem.monitorenv.config.UseCase
import fr.gouv.cacem.monitorenv.domain.entities.authorization.UserAuthorization
import fr.gouv.cacem.monitorenv.domain.hash
import fr.gouv.cacem.monitorenv.domain.repositories.IUserAuthorizationRepository
import org.slf4j.LoggerFactory

@UseCase
class GetAuthorizedUser(
private val userAuthorizationRepository: IUserAuthorizationRepository,
) {
private val logger = LoggerFactory.getLogger(GetAuthorizedUser::class.java)

fun execute(email: String): UserAuthorization {
val hashedEmail = hash(email)

return try {
userAuthorizationRepository.findByHashedEmail(hashedEmail)
} catch (e: Throwable) {
logger.info("User $hashedEmail not found, defaulting to super-user=false")

// By default, a user not found is not super-user
UserAuthorization(
hashedEmail = hashedEmail,
isSuperUser = false,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package fr.gouv.cacem.monitorenv.domain.use_cases.authorization

import fr.gouv.cacem.monitorenv.config.UseCase
import fr.gouv.cacem.monitorenv.domain.hash
import fr.gouv.cacem.monitorenv.domain.repositories.IUserAuthorizationRepository
import org.slf4j.LoggerFactory

@UseCase
class GetIsAuthorizedUser(
private val userAuthorizationRepository: IUserAuthorizationRepository,
) {
private val logger = LoggerFactory.getLogger(GetIsAuthorizedUser::class.java)

fun execute(email: String, isSuperUserPath: Boolean): Boolean {
/**
* If the path is not super-user protected, authorize any logged user
*/
if (!isSuperUserPath) {
return true
}

val hashedEmail = hash(email)

val userAuthorization = try {
userAuthorizationRepository.findByHashedEmail(hashedEmail)
} catch (e: Throwable) {
/**
* If the user is not found in the `UserAuthorizationRepository` and the path
* is super-user protected, reject
*/
return false
}

return userAuthorization.isSuperUser
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package fr.gouv.cacem.monitorenv.domain.use_cases.authorization

import fr.gouv.cacem.monitorenv.config.UseCase
import fr.gouv.cacem.monitorenv.domain.entities.authorization.UserAuthorization
import fr.gouv.cacem.monitorenv.domain.hash
import fr.gouv.cacem.monitorenv.domain.repositories.IUserAuthorizationRepository

@UseCase
class SaveUser(
private val userAuthorizationRepository: IUserAuthorizationRepository,
) {
fun execute(email: String, isSuperUser: Boolean) {
val user = UserAuthorization(
hashedEmail = hash(email),
isSuperUser = isSuperUser,
)

userAuthorizationRepository.save(user)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package fr.gouv.cacem.monitorenv.domain

import java.security.MessageDigest

fun hash(toHash: String) = MessageDigest
.getInstance("SHA-256")
.digest(toHash.toByteArray())
.fold("") { str, it -> str + "%02x".format(it) }
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package fr.gouv.cacem.monitorenv.infrastructure.api.adapters.bff.outputs

import fr.gouv.cacem.monitorenv.domain.entities.authorization.UserAuthorization

data class UserAuthorizationDataOutput(
val isSuperUser: Boolean,
) {
companion object {
fun fromUserAuthorization(userAuthorization: UserAuthorization): UserAuthorizationDataOutput {
return UserAuthorizationDataOutput(
isSuperUser = userAuthorization.isSuperUser,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package fr.gouv.cacem.monitorenv.infrastructure.api.adapters.publicapi.inputs

data class AddUserDataInput(
val email: String,
val isSuperUser: Boolean,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package fr.gouv.cacem.monitorenv.infrastructure.api.endpoints

const val CORRELATION_ID_PRECEDENCE = -1
const val USER_AUTH_FILTER_PRECEDENCE = 1
const val API_KEY_FILTER_PRECEDENCE = 2
const val LOG_REQUEST_PRECEDENCE = 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package fr.gouv.cacem.monitorenv.infrastructure.api.endpoints.bff

import fr.gouv.cacem.monitorenv.domain.hash
import fr.gouv.cacem.monitorenv.domain.use_cases.authorization.GetAuthorizedUser
import fr.gouv.cacem.monitorenv.infrastructure.api.adapters.bff.outputs.UserAuthorizationDataOutput
import fr.gouv.cacem.monitorenv.infrastructure.api.endpoints.security.UserAuthorizationCheckFilter
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.LoggerFactory
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/bff/authorization")
@Tag(name = "APIs for authorization")
class UserAuthorization(
private val getAuthorizedUser: GetAuthorizedUser,
) {
private val logger = LoggerFactory.getLogger(UserAuthorization::class.java)

/**
* This controller will
* - return 200 with the UserAuthorization object if the user authorization is found
* (it passes the filter `UserAuthorizationCheckFilter` - the endpoint is not super-user protected)
* - return an 200 with `isSuperUser=false` if the user authorization is not found
*/
@GetMapping("current")
@Operation(summary = "Get current logged user authorization")
fun getCurrentUserAuthorization(
request: HttpServletRequest,
response: HttpServletResponse,
): UserAuthorizationDataOutput? {
val email: String? = response.getHeader(UserAuthorizationCheckFilter.EMAIL_HEADER)
if (email == null) {
logger.error("Email not found. Rejecting authentication.")

response.status = HttpServletResponse.SC_UNAUTHORIZED

return null
}

val authorizedUser = getAuthorizedUser.execute(email)

// The email is hashed as we don't want to have a clear email in the header
response.setHeader(UserAuthorizationCheckFilter.EMAIL_HEADER, hash(email))

return UserAuthorizationDataOutput.fromUserAuthorization(authorizedUser)
}
}
Loading

0 comments on commit b692bef

Please sign in to comment.