From 172fe19253dfe7aca083d67a55bf40a59dfb152f Mon Sep 17 00:00:00 2001 From: Shashank Verma Date: Sun, 24 Sep 2023 12:33:18 +0530 Subject: [PATCH] Implement google OAuth2 login for authorized API point Signed-off-by: Shashank Verma --- .editorconfig | 3 ++ pom.xml | 8 +++ .../moticlubs/MotiClubsServiceApplication.kt | 11 +++- .../com/mnnit/moticlubs/OpenAPIDefinition.kt | 9 ---- .../moticlubs/repository/UserRepository.kt | 7 +++ .../com/mnnit/moticlubs/utils/Constants.kt | 1 - .../web/security/PathAuthorization.kt | 32 ++++++++++-- .../moticlubs/web/security/SecurityConfig.kt | 50 ++++++++++++------- 8 files changed, 88 insertions(+), 33 deletions(-) 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/utils/Constants.kt b/src/main/kotlin/com/mnnit/moticlubs/utils/Constants.kt index 6c8f576..ab5ad4c 100644 --- a/src/main/kotlin/com/mnnit/moticlubs/utils/Constants.kt +++ b/src/main/kotlin/com/mnnit/moticlubs/utils/Constants.kt @@ -11,7 +11,6 @@ object Constants { const val POSTS_ROUTE = "posts" const val SUPER_ADMIN_ROUTE = "admin" const val CHANNEL_ROUTE = "channel" - const val URL_ROUTE = "url" const val VIEWS_ROUTE = "views" const val REPLY_ROUTE = "reply" 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)) + } } } }