Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reactive Authentication with GraphQL Directive #758

Closed
rowi1de opened this issue Jun 9, 2020 · 3 comments
Closed

Reactive Authentication with GraphQL Directive #758

rowi1de opened this issue Jun 9, 2020 · 3 comments

Comments

@rowi1de
Copy link

rowi1de commented Jun 9, 2020

First, let's a big thank you for you great support and library 👏

I implemented a directive-based authentication with Spring Security.
This issue related to
#663 #692 where the code/examples were taking as a starting point

Factory for the Security Context:

/**
 * Provide GraphQLSecurityContext
 */
@Component
class GraphQLSecurityContextFactory : GraphQLContextFactory<GraphQLSecurityContext> {

    override suspend fun generateContext(
        request: ServerHttpRequest,
        response: ServerHttpResponse
    ): GraphQLSecurityContext {
        val reactorContext =
            coroutineContext[ReactorContext]?.context ?: throw RuntimeException("Reactor Context unavailable")
        val securityContext = reactorContext.getOrDefault<Mono<SecurityContext>>(
            SecurityContext::class.java,
            Mono.error(AccessDeniedException("Security Context unavailable"))
        )!!
        return GraphQLSecurityContext(securityContext = securityContext)
    }
}

class GraphQLSecurityContext(val securityContext: Mono<SecurityContext>) : GraphQLContext

An important difference to the example from previous issues:
securityContext is a Mono<SecurityContext>)

The implementation for the Authorisation checking:

/**
 * Wraps the original Data Fetcher
 * Check the Security Context for the required roles
 */
class AuthorizationDataFetcher(
    /**
     * Original Data fetcher which should be executed
     */
    private val originalDataFetcher: DataFetcher<Any>,
    /**
     * Required Roles for this Data Fetcher Field
     */
    private val requiredRoles: Collection<String>
) : DataFetcher<Any> {

    override fun get(environment: DataFetchingEnvironment): Any {
        if (requiredRoles.isEmpty()) {
            return originalDataFetcher.get(environment)
        }

        val context: GraphQLSecurityContext? = environment.getContext<GraphQLSecurityContext>()
        val securityContext: Mono<SecurityContext> =
            context?.securityContext ?: throw AccessDeniedException("SecurityContext not present")
        val accessCheck = checkRoles(securityContext)

        return accessCheck
            .filter { it.isGranted }
            .map { createResult(originalDataFetcher.get(environment)) }
            .switchIfEmpty(Mono.just(createGraphQLError(environment)))
            .toFuture() // FIXME: how to avoid .block() also not allowed
    }

    private fun checkRoles(securityContext: Mono<SecurityContext>): Mono<AuthorizationDecision> {
        val voter = hasAnyAuthority<Any>(*requiredRoles.toTypedArray())
        return voter.check(securityContext.map { it.authentication }, null).defaultIfEmpty(AuthorizationDecision(false))
    }

    private fun createGraphQLError(environment: DataFetchingEnvironment): DataFetcherResult<Any> {
        val error = SimpleKotlinGraphQLError(
            AccessDeniedException("Role(s) ${requiredRoles.joinToString(separator = ",")} required"),
            listOf(environment.field.sourceLocation),
            environment.executionStepInfo.path.toList()
        )
        return DataFetcherResult.newResult<Any>()
            .error(error)
            .build()
    }

    private fun createResult(data: Any): DataFetcherResult<*> {
        return DataFetcherResult.newResult<Any>()
            .data((data as? CompletableFuture<*>)?.get() ?: data) // FIXME: how to avoid
            .build()
    }
}

To problem/thing I don't like:
How to properly Map the ´Mono` (which could be a mono of any type to indicate Authorization result.

  • Instead of .toFuture() .block() would return the actual value of original data fetcher => which throws block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-2",
  • If the orginalDataFetcher is FunctionDataFetcher it return CompletableFuture
@dariuszkuc
Copy link
Collaborator

I'd recommend to NOT store the Mono and instead store the underlying object, e.g. use null (getOrDefault(xyz, null)) or Optional<SecurityContext> (getOrEmpty(xyz)) instead. This would allow you to await for value in the factory. With the unwrapped value in the GraphQL context, you could still do the same logic in your AuthorizationDataFetcher (i.e. throw AccessDeniedException if it is not present).

@rowi1de
Copy link
Author

rowi1de commented Jun 9, 2020

Good hint, I missed that GraphQLContextFactory supports suspended/coroutines, where DataFetcher.get() does not.
This makes the code much simpler.

reactorContext.getOrDefault<Mono<SecurityContext>>(
            SecurityContext::class.java,
            null
        )!!
GraphQLSecurityContext(securityContext = securityContext.awaitFirstOrNull())
class GraphQLSecurityContext(val securityContext: SecurityContext?) : GraphQLContext

@alpeshvas
Copy link

This should not have been closed as it's not answered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants