zyro23 / grails-spring-websocket

93 stars 28 forks source link

springSecurityService.currentUser in v 1.3.1, Grails 2.5.6 #67

Closed mattnicolls closed 5 years ago

mattnicolls commented 5 years ago

@zyro23, what would your recommended approach be to get spring security working with plugin version 1.3.1 and grails 2.5.6? I realize I can use Principal as a way to identify the current user, but I have services that I call that reference springSecurityUser.currentUser.

Thank you for writing and supporting this plugin! Mat

zyro23 commented 5 years ago

so im guessing you are using the spring security (-core) grails plugin.

what you are looking for is likely spring-security-messaging which ships a SecurityContextChannelInterceptor that ensures the spring security SecurityContextHolder is properly populated for incoming messages (allowing to use springSecurityService.currentUser on the incoming messages' thread).

therefore,

1) add the necessary spring-security dependencies in your build.gradle dependencies section

compile "org.springframework.security:spring-security-config"
compile "org.springframework.security:spring-security-messaging"

2) exclude SecurityFilterAutoConfiguration in your Application class (it conflicts with grails spring-security-core)

@EnableAutoConfiguration(exclude = [SecurityFilterAutoConfiguration])
class Application extends GrailsAutoConfiguration { ... }

3) add a config class extending AbstractSecurityWebSocketMessageBrokerConfigurer and register it as a spring bean, e.g. detected by component-scan or registered in grails-app/conf/spring/resources.groovy

@Configuration
class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    /**
     * here, you can configure client inbound channel security
     * ref. https://docs.spring.io/spring-security/site/docs/4.2.x/reference/html/websocket.html
     */
    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {}

    /**
     * you may want to disable csrf protection depending on your setup
     * ref. https://docs.spring.io/spring-security/site/docs/4.2.x/reference/html/websocket.html#websocket-sameorigin
     */
    @Override
    protected boolean sameOriginDisabled() {
        return true
    }

}

AbstractSecurityWebSocketMessageBrokerConfigurer takes care of defining the SecurityContextChannelInterceptor bean (https://github.com/spring-projects/spring-security/blob/4.2.10.RELEASE/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java#L170-L173) - so you should be good to go.

zyro23 commented 5 years ago

okay well now that i wrote that, i re-read that you are explicitly asking for grails-2.5.x which means spring-security-core-3.2.x. spring-security-messaging was introduced with spring-security-4.0 :( what you could try is take a look at the SecurityContextChannelInterceptor and create/register sth. like that yourself...

mattnicolls commented 5 years ago

Is it sufficient to intercept the call once the Principal is established, then call springSecurityService.reauthenticate passing in principal.name? That approach works, but I'm not sure if that opens up any security holes.

zyro23 commented 5 years ago

no that would not be a good idea as SpringSecurityService#reauthenticate does not clear the security context. that happens for normal web/http requests during processing of the spring security filter chain but as incoming websocket messages are no http requests, you will leak authentications - ☠️ . ref. https://github.com/grails-plugins/grails-spring-security-core/blob/2.x/src/java/grails/plugin/springsecurity/SpringSecurityUtils.java#L585-L602

what you could do is wrap your code in SpringSecurityUtils.doWithAuth(String username, Closure closure) as that ensure clearing/resetting the SecurityContextHolder. ref. https://github.com/grails-plugins/grails-spring-security-core/blob/2.x/src/java/grails/plugin/springsecurity/SpringSecurityUtils.java#L647-L662 that would look sth. like this:

class FooController {

    SpringSecurityService springSecurityService

    @MessageMapping("/hello")
    protected String hello(String world, Principal principal) {
        SpringSecurityUtils.doWithAuth(principal.name) {
            println springSecurityService.currentUser
        }
        return "hello, ${world}!"
    }

}
mattnicolls commented 5 years ago

I only have a few websocket methods to implement for this application anyway, so that solution keeps things nice and concise without messing with the guts of spring-security. Nice work @zyro23 - thank you very much!

mattnicolls commented 5 years ago

@zyro23, thank you again. The intent of gaining access to the User is to prevent unauthorized subscriptions to specific destinations (as show below in listen) so later I could do something like simpMessagingTemplate.convertAndSend("/topic/listen/${id}", message) and it would only go to pre-authorized subscribers.

Questions:

  1. Is the logic in listen() preventing the subscription, or is there a better way?
  2. If (stomp) client subscribes to /app/listen/123, what destination to a use when a message is sent?
class ConversationController {

        SpringSecurityService springSecurityService
    ConversationService conversationService
    SimpMessagingTemplate simpMessagingTemplate

    @SubscribeMapping("/listen/{id}")
    protected String listen(@DestinationVariable String id, Principal principal) {
        SpringSecurityUtils.doWithAuth(principal.name) {

            def user = springSecurityService.currentUser

            if(conversationService.hasReadAccess(id, user)) {
                return "welcome to conversation ${id}, ${user.username}"
            }
            else {
                // todo: how to reject subscription?
            }
        }
    }

    @MessageMapping("/send/{id}")
    protected String send(@DestinationVariable String id, String message, Principal principal) {

        SpringSecurityUtils.doWithAuth(principal.name) {

            def user = springSecurityService.currentUser

            if(conversationService.hasWriteAccess(id, user)) {
                                  // todo: send to conversation specific subscribers
                                  // this destination doesn't work
                simpMessagingTemplate.convertAndSend("/app/listen/${id}", message)
            }
            else {
                // reject message
            }
        }
    }
}
zyro23 commented 5 years ago

unfortunately no, that will not prevent/reject subscriptions. those have already happened then.

what you want is a channel interceptor (which is exactly what AbstractSecurityWebSocketMessageBrokerConfigurer#configureInbound is good for (with spring-security-messaging)..

check the code sample outlined here for kind of a hand-crafted "poor-man's impl.":
https://jira.spring.io/browse/SEC-2546?redirect=false

and a bit of context:
https://github.com/rwinch/spring-websocket-portfolio/commit/5cb4992766c4eceef3c9cc05014a9391dd4a9399#commitcomment-6819849
https://jira.spring.io/browse/SEC-2671?redirect=false

anyway, i would strongly suggest to think about upgrading to grails-3..

update: links fixed (redirect=false)..