Skip to content

Commit

Permalink
Implement google OAuth2 login for authorized API point
Browse files Browse the repository at this point in the history
Signed-off-by: Shashank Verma <[email protected]>
  • Loading branch information
shank03 committed Sep 24, 2023
1 parent a14c15b commit d9f5bae
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 33 deletions.
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
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
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<Void> = exchange.session.flatMap { session -> session.invalidate() }
}

fun main(args: Array<String>) {
runApplication<MotiClubsServiceApplication>(*args)
Expand Down
9 changes: 0 additions & 9 deletions src/main/kotlin/com/mnnit/moticlubs/OpenAPIDefinition.kt
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -33,6 +25,5 @@ import io.swagger.v3.oas.annotations.servers.Server
description = "Production",
),
],
security = [SecurityRequirement(name = "Firebase Auth")],
)
class OpenAPIDefinition
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class ClubController(
@Operation(summary = "Updates the list of urls for the club")
fun updateUrls(
@RequestBody dto: SaveUrlsDTO,
@RequestParam clubId: Long
@RequestParam clubId: Long,
): Mono<ResponseEntity<List<Url>>> = pathAuthorization
.clubAuthorization(clubId)
.flatMap {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ class UserRepository(
User::class.java,
)

@Transactional
fun findByEmail(email: String): Mono<User> = db
.selectOne(
Query.query(Criteria.where(User::email.name).`is`(email)).limit(1),
User::class.java,
)

@Transactional
fun findAll(): Flux<User> = db.select(Query.empty(), User::class.java)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,61 @@ 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 {
private val LOGGER = ServiceLogger.getLogger(PathAuthorization::class.java)
}

fun userAuthorization(): Mono<Long> = 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"))
}

LOGGER.info("userAuthorization: success [$userId]")
Mono.just(userId)
}

private fun validateOidcUser(user: DefaultOidcUser): Mono<Pair<Long, Boolean>> {
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<Long> = userAuthorization()
.flatMap { uid ->
val admin = Admin(cid, uid)
Expand Down
50 changes: 32 additions & 18 deletions src/main/kotlin/com/mnnit/moticlubs/web/security/SecurityConfig.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -30,6 +31,8 @@ class SecurityConfig(
"/swagger",
"/webjars/swagger-ui",
"/v3/api-docs",
"/login",
"/logout",
)
}

Expand Down Expand Up @@ -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(
Expand All @@ -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))
}
}
}
}
Expand Down
Binary file modified src/main/resources/secrets.yml.gpg
Binary file not shown.

0 comments on commit d9f5bae

Please sign in to comment.