spring-bootspring-securitywebsocketgraphql-javanetflix-dgs

How to handle security (authn/authz) with DGS Subscriptions and spring security?


I'm mirroring a question asked directly on GitHub here.

I'm up to the point where if I authorize("/subscriptions",permitAll) while configuring my SecurityWebFilterChain then I can successfully use my subscription queries. However, that removes all the security. I would have liked to do: authorize("/subscriptions",hasAuthority("access"))

Anyway, now I need to make sur that the user is properly authenticated and authorized. I use ReactiveMetodSecurity with @PreAuthorize("hasAuthority('read')") or hasPermission(#id, 'entity', 'read:restricted') directly on the @DgsSubscription method. This works in a way: hasAuthority is triggered although it responds with false. As far as I know that's because the Authentication object has not been initialized with the token.

Anonymous authentication

It's most likely because there is no default behavior to intercept the connection_init message that contains the token.

Thus I'm wondering: how can I fetch that connection_init message and set the Authentication so that it's picked up by spring ?

Thanks

EDIT 1: I've been able to successfully authenticate from a SecurityWebFilterChain point of view. The Authentication object however is not available in the @DgsSubscription. I can access it though using the workaround explained here. That means I now have a solution although this won't be working with a connection_init provided Authorization. Also it will require me to add a level to my objects to have the PreAuthorization work.


Solution

  • Here is was I was able to do with the auth header in the handshake request:

    @Component
    class DgsSecurityContextBuilder : DgsReactiveCustomContextBuilderWithRequest<Authentication> {
        override fun build(
            extensions: Map<String, Any>?,
            headers: HttpHeaders?,
            serverRequest: ServerRequest?
        ): Mono<Authentication> = ReactiveSecurityContextHolder.getContext().map { it.authentication }
    }
    
    @DgsComponent
    class AggregateSubscriptions(val handler: AggregateSubscriptionsHandler) {
        @DgsSubscription
        fun aggregateNotifications(
            @InputArgument id: UUID,
            @InputArgument notificationTypes: List<NotificationType>?,
            dfe: DataFetchingEnvironment
        ) = let {
            val auth = DgsContext.getCustomContext<Authentication>(dfe)
            handler.aggregateNotifications(id, notificationTypes, auth)
        }
    }
    
    @Component
    class AggregateSubscriptionsHandler {
        @PreAuthorize("@myPermissionEvaluator.hasAuthority(#auth, '$AGGREGATE_READ')")
        fun aggregateNotifications(id: UUID, notificationTypes: List<NotificationType>?, auth: Authentication) =
            Flux.interval(Duration.ofMillis(1000))
                .map {
                    Notification.Builder()
                        .withAggregateId(id.toString())
                        .withNotificationType(NotificationType.AdvertiserAdded)
                        .withPayload(listOf(KeyValue.Builder().withKey("t").withValue(it.toString()).build()))
                        .build()
                }
    }
    

    That's the only way I found to currently handle authz in subscription requests

    Hopefully we'll get a fix soon that will allow us to use the connection_init message.

    See: https://github.com/Netflix/dgs-framework/issues/1442 and https://github.com/Netflix/dgs-framework/issues/450