Interface AuthenticationPlugin.SessionAuthenticator

Enclosing interface:
AuthenticationPlugin

public static interface AuthenticationPlugin.SessionAuthenticator
An instance of this interface shall be returned upon invocation of AuthenticationPlugin.newSessionAuthenticator(). The implementation may choose whether it is a singleton, or if a new instance is returned per invocation (which then means there is a unique instance per WebSocket/MatsSocket Session, thus you can hold values for the session there).
  • Method Details

    • checkOrigin

      default boolean checkOrigin(String originHeaderValue)
      Implement this if you want to do a check on the Origin header value while the initial WebSocket Upgrade/Handshake request is still being processed. This invocation is directly forwarded from ServerEndpointConfig.Configurator.checkOrigin(String). Note that you do not have anything else to go by here, it is a static check. If you want to evaluate other headers (or probably more relevant for web clients, parameters), to decide upon allowed Origins, you can get the Origin header from the HandshakeRequest within the checkHandshake(ServerEndpointConfig, HandshakeRequest, HandshakeResponse) checkHandshake(..)} - that is however one step later in the establishing of the socket (response is already sent, so if you decide against the session there, the socket will be opened, then immediately closed).

      The default implementation returns true, i.e. letting all connections through this step.

      Parameters:
      originHeaderValue - which Origin the client connects from - this is mandatory for web browsers to set.
      Returns:
      whether this Origin shall be allowed to connect. Default implementation returns true.
    • checkHandshake

      default boolean checkHandshake(javax.websocket.server.ServerEndpointConfig config, javax.websocket.server.HandshakeRequest request, javax.websocket.HandshakeResponse response)
      Implement this if you want to do a check on any other header values, e.g. Authorization or Cookies, while the initial WebSocket Upgrade/Handshake request is still being processed - before the response is sent. This invocation is directly forwarded from ServerEndpointConfig.Configurator.modifyHandshake(ServerEndpointConfig, HandshakeRequest, HandshakeResponse). You may add headers to the response object. If this method returns false or throws anything, the WebSocket request will immediately be terminated.

      NOTE! We would ideally want to evaluate upon the initial HTTP WebSocket handshake request whether we want to talk with this client. The best solution would be an Authorization header on this initial HTTP request. However, since it is not possible to add headers via the WebSocket API in web browsers, this does not work. Secondly, we could have added it as a URI parameter, but this is strongly discouraged. We can use cookies to achieve somewhat of the same effect (setting it via document.cookie before running new WebSocket(..)), but since it is possible that you'd want to connect to a different domain for the WebSocket than the web page was served from, this is is not ideal as a general solution. A trick is implemented in MatsSocket, whereby it can do a XmlHttpRequest to a HTTP service that is running on the same domain as the WebSocket, which can move the Authorization header to a Cookie, which will be sent along with the WebSocket Handshake request - and can thus be checked here.

      The default implementation returns true, i.e. letting all connections through this step.

      Parameters:
      config - the ServerEndpointConfig instance.
      request - the HTTP Handshake Request
      response - the HTTP Handshake Response, upon which it is possible to set headers.
      Returns:
      true if the connection should be let through, false if not.
    • onOpen

      default boolean onOpen(javax.websocket.Session webSocketSession, javax.websocket.server.ServerEndpointConfig config)
      Invoked straight after the HTTP WebSocket handshake request/response is performed, in the Endpoint.onOpen(Session, EndpointConfig) invocation from the WebSocket ServerContainer. If this method returns false or throws anything, the WebSocket request will immediately be terminated by invocation of Session.close() - the implementation of this method may also itself do such a close. If you do anything crazy with the Session object which will interfere with the MatsSocket messages, that's on you. One thing that could be of interest, is to increase the max idle timeout - which will have effect wrt. to the initial authentication message that is expected, as the timeout setting before authenticated session is set to a quite small value by default: 2.5 seconds (after auth, the timeout is increased). This short timeout is meant to make it slightly more difficult to perform DoS attacks by opening many connections and then not send the initial auth-containing message. The same goes for max text message buffer size, as that is also set low until authenticated: 20KiB.

      The default implementation returns true, i.e. letting all connections through this step.

      Parameters:
      webSocketSession - the WebSocket API Session instance.
      config - the ServerEndpointConfig instance.
      Returns:
      true if the connection should be let through, false if not.
    • initialAuthentication

      Invoked when the MatsSocket initially connects (or reconnects) over WebSocket, and needs to be authenticated by the Authorization string supplied via the initial set ("pipeline") of message from the client, typically in the HELLO message (which needs to present in the initial pipeline). At this point, there is obviously do not a Principal yet, so this AuthenticationPlugin.SessionAuthenticator needs to supply it.

      For every next incoming pipeline of messages, the method reevaluateAuthentication(...) is invoked - which, depending on authentication scheme in use, might just always reply "stillValid", thus trusting the initial authentication - or for e.g. OAuth schemes where the token has an expiry time, actually evaluate this expiry time. If that method decides that the current Authorization value isn't good enough, the Client will be asked to provide a new one (refresh the token).

      For outgoing messages from the Server (that is, Replies to Client-to-Server Requests, and Server-to-Client Sends and Requests), the method reevaluateAuthenticationForOutgoingMessage(AuthenticationContext, String, Principal, long) is invoked. Again, it depends on the authentication scheme in use: Either trust the initial authentication, or re-evaluate whether the current token which the Server has is valid enough. And again, if that method decides that the current Authorization value isn't good enough, the Client will be asked to provide a new one (refresh the token).

      Note: If this method returns AuthenticationPlugin.AuthenticationContext.invalidAuthentication(String), the connection is immediately closed with MatsSocketServer.MatsSocketCloseCodes.VIOLATED_POLICY.

      NOTE! Read the JavaDoc for checkHandshake(..) wrt. evaluating the actual HTTP Handshake Request, as you might want to do auth already there.

      Parameters:
      context - were you may get additional information (the HandshakeRequest and the WebSocket Session), and can create AuthenticationPlugin.AuthenticationResult to return.
      authorizationHeader - the string value to evaluate for being a valid authorization, in any way you fanzy - it is supplied by the client, where you also will have to supply a authentication plugin that creates the strings that you here will evaluate.
      Returns:
      an AuthenticationPlugin.AuthenticationResult, which you get from any of the method on the AuthenticationPlugin.AuthenticationContext.
    • reevaluateAuthentication

      AuthenticationPlugin.AuthenticationResult reevaluateAuthentication(AuthenticationPlugin.AuthenticationContext context, String authorizationHeader, Principal existingPrincipal)
      Invoked on every subsequent "pipeline" of incoming messages (including every time the Client supplies a new Authorization string). The reason for separating this out in a different method is that you should want to optimize it heavily (read 'Note' below): If the 'existingPrincipal' is still valid (and with that also the userId that was supplied at AuthenticationPlugin.AuthenticationContext.authenticated(Principal, String)), then you can return the result of AuthenticationPlugin.AuthenticationContext.stillValid(). A correct, albeit possibly slow, implementation of this method is to just forward the call to initialAuthentication(..) - it was however decided to not default-implement this solution in the interface, so that you should consider the implications of this.

      If the method returns invalidAuthentication(), the server will not process the incoming pipeline, and instead ask the client for re-auth, and when this comes in and is valid, the processing will ensue (it holds the already provided messages so that client does not need to deliver them again). Notice: As opposed to the initialAuthentication, replying invalidAuthentication() does NOT immediately close the WebSocket, but only implies that the client needs to supply a new authorization headers before processing will go on.

      NOTE! You might want to hold on to the 'authorizationHeader' between the invocations to quickly evaluate whether it has changed: In e.g. an OAuth setting, where the authorizationHeader is a bearer access token, you could shortcut evaluation: If it has not changed, then you might be able to just evaluate whether it has expired by comparing a timestamp that you stored when first evaluating it, towards the current time. However, if the authorizationHeader (token) has changed, you would do full evaluation of it, as with initialAuthentication(..).

      Parameters:
      context - were you may get additional information (the HandshakeRequest and the WebSocket Session), and can create AuthenticationPlugin.AuthenticationResult to return.
      authorizationHeader - the string value to evaluate for being a valid authorization, in any way you fanzy - it is supplied by the client, where you also will have to supply a authentication plugin that creates the strings that you here will evaluate.
      existingPrincipal - The Principal that was returned with the last authentication (either initial or reevaluate).
      Returns:
      an AuthenticationPlugin.AuthenticationResult, which you get from any of the method on the AuthenticationPlugin.AuthenticationContext.
    • reevaluateAuthenticationForOutgoingMessage

      default AuthenticationPlugin.AuthenticationResult reevaluateAuthenticationForOutgoingMessage(AuthenticationPlugin.AuthenticationContext context, String authorizationHeader, Principal existingPrincipal, long lastAuthenticatedTimestamp)
      This method is invoked each time the server wants to send a message to the client - either a REPLY to a request from the Client, or a Server-to-Client SEND or MatsSocketServer.request(String, String, String, Object, String, String, byte[]) REQUEST}.

      If the method returns invalidAuthentication(), the server will now hold this delivery, and instead ask the client for re-auth, and when this comes in and is valid, the delivery will ensue. Notice: As opposed to the initialAuthentication, replying invalidAuthentication() does NOT immediately close the WebSocket, but only implies that the client needs to supply a new authorization headers before getting the outbound messages.

      It is default-implemented to invoke reevaluateAuthentication(..), but the reason for separating this out in (yet a) different method is that you might want to add a bit more slack wrt. expiry if OAuth-style auth is in play: Let's say a request from the Client is performed right at the limit of the expiry of the token (where the "slack" is set to a ridiculously low 10 seconds). But let's now imagine that this request takes 11 seconds to complete. If you do not override the default implementation, the default implementation will forward to reevaluateAuthentication, get invalidAuthentication() as answer, and thus initiate a full round-trip to the client and to the auth-server to get a new token. However, considering that this SessionAuthenticator was OK with the authentication when the request came it, and that by the situation's definition the WebSocket connection has not gone done in the mean time (otherwise it would have had to do full initial auth), one could argue that a Reply should have some extra room wrt. expiry. For Server-to-Client messages SEND and REQUEST, the same mechanism is employed, and I'd argue that the same arguments hold: This SessionAuthenticator was happy with the authentication some few minutes ago and the connection has not broken in the meantime, so you should work pretty hard to set up a situation where a REQUEST from the Server or some data using SEND from the Server would be a large security consideration.

      You get provided the last time this SessionAuthenticator was happy with this exact Authorization value as parameter 'lastAuthenticatedTimestamp' - that is, this timestamp is reset each time initialAuthentication(..) and reevaluateAuthentication answers any of authenticated or AuthenticationPlugin.AuthenticationContext.stillValid().

      An ok implementation is therefore to simply answer stillValid() if the 'lastAuthenticatedTimestamp' parameter is less than X minutes ago, otherwise answer invalidAuthentication() (and thus the client will be asked to supply new auth). Another slightly more elaborate implementation is to store the token and the actual expiry time of the token in reevaluateAuthentication(..), and if the token is the same with this invocation, just evaluate the expiry time against current time, but add X minutes slack to this evaluation.

      Parameters:
      context - were you may get additional information (the HandshakeRequest and the WebSocket Session), and can create AuthenticationPlugin.AuthenticationResult to return.
      authorizationHeader - the string value to evaluate for being a valid authorization, in any way you fanzy - it is supplied by the client, where you also will have to supply a authentication plugin that creates the strings that you here will evaluate.
      existingPrincipal - The Principal that was returned with the last authentication (either initial or reevaluate).
      lastAuthenticatedTimestamp - the millis-since-epoch of when this exact 'authorizationHeader' was deemed OK by this same SessionAuthenticator.
      Returns:
      an AuthenticationPlugin.AuthenticationResult, which you get from any of the method on the AuthenticationPlugin.AuthenticationContext - preferably either stillValid() or invalidAuthentication() based on whether to let this Server-to-Client message through, or force the Client to reauthenticate before getting the message.
    • authorizeUserForTopic

      default boolean authorizeUserForTopic(AuthenticationPlugin.AuthenticationContext context, String topicId)
      Decide whether the specified Principal/User should be allowed to subscribe to the specified Topic.

      Note: The 'authorizationHeader' is already validated by one of the authentication methods, which have supplied the Principal and userId present in the supplied AuthenticationPlugin.AuthenticationContext.

      The default implementation return true, i.e. letting all users subscribe to all Topics.

      Parameters:
      context - the AuthenticationPlugin.AuthenticationContext for reference - this has getters for the info you should require to decide whether the current Client user should be allowed to subscribe to the given topic.
      topicId - the Id of the Topic the client tries to subscribe to
      Returns:
      true if the user should be allowed to subscribe to the Topic, false if the user should not be allowed to subscribe - he will then not get any messages sent over the Topic.