diff --git a/.editorconfig b/.editorconfig
index 86a1d30..98652f5 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -20,3 +20,6 @@ ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ij_kotlin_import_nested_classes = false
ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
ij_kotlin_continuation_indent_for_expression_bodies = false
+
+[*.yml]
+indent_size = 2
diff --git a/pom.xml b/pom.xml
index 1f41351..bc0cd7c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,6 +31,14 @@
org.springframework.boot
spring-boot-starter-data-r2dbc
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+
org.springframework.boot
spring-boot-starter-security
diff --git a/src/main/kotlin/com/mnnit/moticlubs/MotiClubsServiceApplication.kt b/src/main/kotlin/com/mnnit/moticlubs/MotiClubsServiceApplication.kt
index 4f4c8b7..7b13ae6 100644
--- a/src/main/kotlin/com/mnnit/moticlubs/MotiClubsServiceApplication.kt
+++ b/src/main/kotlin/com/mnnit/moticlubs/MotiClubsServiceApplication.kt
@@ -1,5 +1,6 @@
package com.mnnit.moticlubs
+import io.swagger.v3.oas.annotations.Hidden
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
@@ -7,13 +8,21 @@ import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories
import org.springframework.scheduling.annotation.EnableScheduling
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.server.ServerWebExchange
+import reactor.core.publisher.Mono
@SpringBootApplication(exclude = [ReactiveUserDetailsServiceAutoConfiguration::class])
@EnableR2dbcRepositories
@ConfigurationPropertiesScan
@EnableCaching
@EnableScheduling
-class MotiClubsServiceApplication
+class MotiClubsServiceApplication {
+
+ @GetMapping("/logout")
+ @Hidden
+ fun logout(exchange: ServerWebExchange): Mono = exchange.session.flatMap { session -> session.invalidate() }
+}
fun main(args: Array) {
runApplication(*args)
diff --git a/src/main/kotlin/com/mnnit/moticlubs/OpenAPIDefinition.kt b/src/main/kotlin/com/mnnit/moticlubs/OpenAPIDefinition.kt
index 3c2c040..194edc5 100644
--- a/src/main/kotlin/com/mnnit/moticlubs/OpenAPIDefinition.kt
+++ b/src/main/kotlin/com/mnnit/moticlubs/OpenAPIDefinition.kt
@@ -1,18 +1,10 @@
package com.mnnit.moticlubs
import io.swagger.v3.oas.annotations.OpenAPIDefinition
-import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.info.Contact
import io.swagger.v3.oas.annotations.info.Info
-import io.swagger.v3.oas.annotations.security.SecurityRequirement
-import io.swagger.v3.oas.annotations.security.SecurityScheme
import io.swagger.v3.oas.annotations.servers.Server
-@SecurityScheme(
- type = SecuritySchemeType.HTTP,
- name = "Firebase Auth",
- scheme = "Bearer",
-)
@OpenAPIDefinition(
info = Info(
title = "MotiClubs Service",
@@ -33,6 +25,5 @@ import io.swagger.v3.oas.annotations.servers.Server
description = "Production",
),
],
- security = [SecurityRequirement(name = "Firebase Auth")],
)
class OpenAPIDefinition
diff --git a/src/main/kotlin/com/mnnit/moticlubs/repository/UserRepository.kt b/src/main/kotlin/com/mnnit/moticlubs/repository/UserRepository.kt
index 90f645f..7ff5336 100644
--- a/src/main/kotlin/com/mnnit/moticlubs/repository/UserRepository.kt
+++ b/src/main/kotlin/com/mnnit/moticlubs/repository/UserRepository.kt
@@ -26,6 +26,13 @@ class UserRepository(
User::class.java,
)
+ @Transactional
+ fun findByEmail(email: String): Mono = db
+ .selectOne(
+ Query.query(Criteria.where(User::email.name).`is`(email)).limit(1),
+ User::class.java,
+ )
+
@Transactional
fun findAll(): Flux = db.select(Query.empty(), User::class.java)
diff --git a/src/main/kotlin/com/mnnit/moticlubs/web/security/PathAuthorization.kt b/src/main/kotlin/com/mnnit/moticlubs/web/security/PathAuthorization.kt
index a1ee1c3..18d5b26 100644
--- a/src/main/kotlin/com/mnnit/moticlubs/web/security/PathAuthorization.kt
+++ b/src/main/kotlin/com/mnnit/moticlubs/web/security/PathAuthorization.kt
@@ -3,18 +3,23 @@ package com.mnnit.moticlubs.web.security
import com.mnnit.moticlubs.dao.Admin
import com.mnnit.moticlubs.repository.AdminRepository
import com.mnnit.moticlubs.repository.SuperAdminRepository
+import com.mnnit.moticlubs.repository.UserRepository
import com.mnnit.moticlubs.utils.ServiceLogger
import com.mnnit.moticlubs.utils.UnauthorizedException
import com.mnnit.moticlubs.utils.getReqId
import com.mnnit.moticlubs.utils.putReqId
+import com.nimbusds.jwt.JWTParser
import org.springframework.security.core.context.ReactiveSecurityContextHolder
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
+import reactor.kotlin.core.publisher.switchIfEmpty
@Component
class PathAuthorization(
private val adminRepository: AdminRepository,
private val superAdminRepository: SuperAdminRepository,
+ private val userRepository: UserRepository,
) {
companion object {
@@ -22,11 +27,18 @@ class PathAuthorization(
}
fun userAuthorization(): Mono = ReactiveSecurityContextHolder.getContext()
- .flatMap { ctx ->
- val token = ctx.authentication.principal as AuthenticationToken
- val userId = token.userId ?: return@flatMap Mono.error(UnauthorizedException("Missing user ID claim"))
+ .map { it.authentication.principal }
+ .flatMap {
+ when (it) {
+ is DefaultOidcUser -> validateOidcUser(it)
+ is AuthenticationToken -> Mono.just(Pair(it.userId, it.isEmailVerified))
+ else -> Mono.error(UnauthorizedException("Invalid authorization means"))
+ }
+ }
+ .flatMap { (userId, isEmailVerified) ->
+ userId ?: return@flatMap Mono.error(UnauthorizedException("Missing user ID claim"))
- if (!token.isEmailVerified) {
+ if (!isEmailVerified) {
return@flatMap Mono.error(UnauthorizedException("Please verify email ID"))
}
@@ -34,6 +46,18 @@ class PathAuthorization(
Mono.just(userId)
}
+ private fun validateOidcUser(user: DefaultOidcUser): Mono> {
+ val jwtClaims = JWTParser.parse(user.idToken.tokenValue).jwtClaimsSet
+ val isEmailVerified = jwtClaims.claims["email_verified"]?.toString()?.toBoolean() ?: false
+ val email = jwtClaims.claims["email"]?.toString()
+
+ email ?: return Mono.error(UnauthorizedException("Missing email"))
+
+ return userRepository.findByEmail(email)
+ .map { Pair(it.uid, isEmailVerified) }
+ .switchIfEmpty { Mono.error(UnauthorizedException("Invalid user")) }
+ }
+
fun clubAuthorization(cid: Long): Mono = userAuthorization()
.flatMap { uid ->
val admin = Admin(cid, uid)
diff --git a/src/main/kotlin/com/mnnit/moticlubs/web/security/SecurityConfig.kt b/src/main/kotlin/com/mnnit/moticlubs/web/security/SecurityConfig.kt
index 28ebfd1..77512b7 100644
--- a/src/main/kotlin/com/mnnit/moticlubs/web/security/SecurityConfig.kt
+++ b/src/main/kotlin/com/mnnit/moticlubs/web/security/SecurityConfig.kt
@@ -1,5 +1,6 @@
package com.mnnit.moticlubs.web.security
+import com.mnnit.moticlubs.utils.Constants.BASE_PATH
import com.mnnit.moticlubs.utils.ServiceLogger
import com.mnnit.moticlubs.utils.UnauthorizedException
import com.mnnit.moticlubs.utils.putReqId
@@ -30,6 +31,8 @@ class SecurityConfig(
"/swagger",
"/webjars/swagger-ui",
"/v3/api-docs",
+ "/login",
+ "/logout",
)
}
@@ -70,12 +73,14 @@ class SecurityConfig(
.authorizeExchange { spec ->
spec.pathMatchers(
"/actuator/**",
- "/swagger/**",
- "/webjars/swagger-ui/**",
- "/v3/api-docs/**",
+ "/login/**",
).permitAll()
.anyExchange().authenticated()
}
+ .oauth2Login { }
+ .oauth2Client { }
+ .oauth2ResourceServer { it.jwt { } }
+ .logout { }
.build()
private fun firebaseAuthTokenFilter(keyProvider: KeyProvider): AuthenticationWebFilter = AuthenticationWebFilter(
@@ -91,28 +96,37 @@ class SecurityConfig(
putReqId(exchange.request.id)
val reqPath = exchange.request.path.value()
- if (AUTH_WHITELIST_PATH
- .map { "$contextPath$it" }
- .any { reqPath.startsWith(it) }
- ) {
- return@setServerAuthenticationConverter Mono.empty()
- }
LOGGER.info("attempt path: ${exchange.request.method.name()} ${exchange.request.path.value()}")
val authHeader = exchange.request.headers[HttpHeaders.AUTHORIZATION]
?.first()
?.replace("Bearer", "")
?.trim()
- ?: return@setServerAuthenticationConverter Mono.error(
- UnauthorizedException("Missing firebase auth token"),
- )
- try {
- val token = keyProvider.verifyJwt(authHeader)
- Mono.just(FirebaseAuthentication(token))
- } catch (e: Exception) {
- LOGGER.warn("Invalid auth token: ${e.localizedMessage}")
- Mono.error(UnauthorizedException(e.localizedMessage))
+ exchange.session.flatMap { session ->
+ val validSession = session.isStarted && !session.isExpired
+
+ authHeader ?: return@flatMap when {
+ AUTH_WHITELIST_PATH
+ .map { "$contextPath$it" }
+ .any { reqPath.startsWith(it) } -> Mono.empty()
+
+ reqPath.startsWith("/$BASE_PATH") -> if (validSession) {
+ Mono.empty()
+ } else {
+ Mono.error(UnauthorizedException("Missing firebase auth token"))
+ }
+
+ else -> Mono.error(UnauthorizedException("Path not whitelisted"))
+ }
+
+ try {
+ val token = keyProvider.verifyJwt(authHeader)
+ Mono.just(FirebaseAuthentication(token))
+ } catch (e: Exception) {
+ LOGGER.warn("Invalid auth token: ${e.localizedMessage}")
+ Mono.error(UnauthorizedException(e.localizedMessage))
+ }
}
}
}