LivePersonInc / dropwizard-websockets

MIT License
61 stars 28 forks source link

Support to Dropwizard authentication #15

Open danielborges93 opened 7 years ago

danielborges93 commented 7 years ago

Hi!

It is possible use the default io.dropwizard.auth with dropwizard-websockets? In our project we are using OAuth authenticantion in resources.

I'm testing this class

@Timed(name = "timed")
@Metered(name = "metered")
@ExceptionMetered(name = "exception")
@ServerEndpoint("/socket")
@PermitAll
public class NotificationSocket {

    @OnOpen
    public void myOnOpen(final Session session) throws IOException {
        System.out.println("Principal: " + session.getUserPrincipal());
        session.getAsyncRemote().sendText("welcome");
    }

    @OnMessage
    public void myOnMsg(final Session session, String message) {
        session.getAsyncRemote().sendText(message);
    }

    @OnClose
    public void myOnClose(final Session session, CloseReason cr) {
    }

}

with this simple node script:

var WebSocket = require('ws')

const options = {
    headers: {
        'Authorization': 'Bearer token' // token is a system generated string
    }
}

const ws = new WebSocket('ws://localhost:58040/socket', options)
ws.on('message', function incoming(data) {
  console.log(data);
})

and the Principal always is null.

I want to associate each session ith the logged user.

Thanks!

concernedrat commented 6 years ago

@danielborges93 After the Websocket handshake you no longer have access to the HttpSession object, to get the HttpSession you should provide a ServerEndpoint.Configurator and intercept the HttpSession object in the modifyHandshake method and pass that object to your Websocket Endpoint instance (it should have a HttpSession setter btw), here is a working example (I am using Dropwizard-Guicey as DI):

public class MintyWebsocketConfigurator<A extends Application> extends ServerEndpointConfig.Configurator {

    private A dropwizardApp;

    private HttpSession httpSession;

    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {

        httpSession = (HttpSession) request.getHttpSession();

        super.modifyHandshake(sec, request, response);
    }

    public MintyWebsocketConfigurator(A dropwizardApp) {
        this.dropwizardApp = dropwizardApp;
    }

    @Override
    public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {

        final T websocketEndpoint = InjectorLookup.getInjector(dropwizardApp).get().getInstance(endpointClass);

        if (websocketEndpoint instanceof HttpSessionAwareWebsocketResource) {

            ((HttpSessionAwareWebsocketResource) websocketEndpoint).setHttpSession(httpSession);

        }

        return websocketEndpoint;
    }
}
Ganon-M commented 6 years ago

I spent a day or two dealing with this and would like to share the best solution I could come up with. I used the answer by @georgerb as a basis, thanks. This solution doesn't depend upon a specific DI framework and avoids the race condition between modifyHandshake and getEndpointInstance present in the previously suggested solution (AFAICT the configurator instance is shared between request threads). In an ideal world we would somehow modify the session object to set the Principal directly, but I couldn't find a way to do this. As such, the main source of ugliness in this solution is having to put the Principal into the user properties map. I would be very grateful if someone had a solution to that

Any feedback/improvements are very welcome

A few things to note:

We implement a configurator that takes e.g. a Basic Auth authenticator, extracts the credentials/token from the upgrade request, verifies them, then adds the user to the session properties so they can be accessed in the Endpoint class

public class AuthenticatedEndpointConfigurator
    extends ServerEndpointConfig.Configurator {

    public AuthenticatedEndpointConfigurator(
                    final SomeAuthenticatorImplementation authenticator
    ) {
        this.authenticator = authenticator;
    }

    @Override
    public void modifyHandshake(
                final ServerEndpointConfig sec,
                final HandshakeRequest request,
                final HandshakeResponse response
    ) {
        try {
            final User user = authenticator.authenticate(
                credentialsFromQueryString(
                    request.getQueryString()
                )
            ).orElse(() ->
                // handle authentication failure here
            );
            sec.getUserProperties().put(User.class.getName(), user);
            super.modifyHandshake(sec, request, response);
        } catch (final AuthenticationException e) {
                // handle invalid/malformed credentials here
        }
    }

    private final SomeAuthenticatorImplementation authenticator;
}

Example endpoint that extracts the user and sets the member variable so it can be accessed throughout the lifetime of the socket connection

@ServerEndpoint("/some/endpoint")
public class SomeEndpoint {
    @OnOpen
    public void onOpen(final Session session) {
        user = (User) session.getUserProperties().get(User.class.getName());
    }

    @OnClose
    public void close(Session session) {
        session.getAsyncRemote().sendText("Goodbye, " + user.getName());
    }

    // There is a unique instance of this class per session, but any thread can
    // call the methods here so instance fields should be volatile
    private volatile User user;
}

Then in our Application class we have

    @Override
    public void initialize(
        final Bootstrap<SomeConfiguration> bootstrap
    ) {
        websocketBundle = new WebsocketBundle(
            // just use a default configurator here
            new ServerEndpointConfig.Configurator()
        );
        bootstrap.addBundle(websocketBundle);
    }

    @Override
    public void run(
                final SomeConfiguration configuration,
                final Environment environment
    ) {
        final ServerEndpointConfig sec =
            ServerEndpointConfig.Builder
                .create(SomeEndpoint.class, "/some/endpoint")
                .configurator(
                    new AuthenticatedEndpointConfigurator(authenticator)
                )
                .build();
        websocketBundle.addEndpoint(sec);
    }