Package io.mats3.matssocket
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 Summary
Modifier and TypeMethodDescriptiondefault boolean
authorizeUserForTopic
(AuthenticationPlugin.AuthenticationContext context, String topicId) Decide whether the specified Principal/User should be allowed to subscribe to the specified Topic.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.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.initialAuthentication
(AuthenticationPlugin.AuthenticationContext context, String authorizationHeader) 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).default boolean
onOpen
(javax.websocket.Session webSocketSession, javax.websocket.server.ServerEndpointConfig config) Invoked straight after the HTTP WebSocket handshake request/response is performed, in theEndpoint.onOpen(Session, EndpointConfig)
invocation from the WebSocketServerContainer
.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).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-ClientSEND
orMatsSocketServer.request(String, String, String, Object, String, String, byte[])
REQUEST}.
-
Method Details
-
checkOrigin
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 fromServerEndpointConfig.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 theHandshakeRequest
within thecheckHandshake(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 returnstrue
, 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 fromServerEndpointConfig.Configurator.modifyHandshake(ServerEndpointConfig, HandshakeRequest, HandshakeResponse)
. You may add headers to the response object. If this method returnsfalse
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 viadocument.cookie
before runningnew 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 returnstrue
, i.e. letting all connections through this step.- Parameters:
config
- theServerEndpointConfig
instance.request
- the HTTP Handshake Requestresponse
- 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 theEndpoint.onOpen(Session, EndpointConfig)
invocation from the WebSocketServerContainer
. If this method returnsfalse
or throws anything, the WebSocket request will immediately be terminated by invocation ofSession.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 toincrease 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 formax text message buffer size
, as that is also set low until authenticated: 20KiB. The default implementation returnstrue
, i.e. letting all connections through this step.- Parameters:
webSocketSession
- the WebSocket APISession
instance.config
- theServerEndpointConfig
instance.- Returns:
true
if the connection should be let through,false
if not.
-
initialAuthentication
AuthenticationPlugin.AuthenticationResult initialAuthentication(AuthenticationPlugin.AuthenticationContext context, String authorizationHeader) 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 thisAuthenticationPlugin.SessionAuthenticator
needs to supply it. For every next incoming pipeline of messages, the methodreevaluateAuthentication(...)
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 methodreevaluateAuthenticationForOutgoingMessage(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 returnsAuthenticationPlugin.AuthenticationContext.invalidAuthentication(String)
, the connection is immediately closed withMatsSocketServer.MatsSocketCloseCodes.VIOLATED_POLICY
. NOTE! Read the JavaDoc forcheckHandshake(..)
wrt. evaluating the actual HTTP Handshake Request, as you might want to do auth already there.- Parameters:
context
- were you may get additional information (theHandshakeRequest
and the WebSocketSession
), and can createAuthenticationPlugin.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 theAuthenticationPlugin.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 atAuthenticationPlugin.AuthenticationContext.authenticated(Principal, String)
), then you can return the result ofAuthenticationPlugin.AuthenticationContext.stillValid()
. A correct, albeit possibly slow, implementation of this method is to just forward the call toinitialAuthentication(..)
- 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 returnsinvalidAuthentication()
, 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 theinitialAuthentication
, 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 withinitialAuthentication(..)
.- Parameters:
context
- were you may get additional information (theHandshakeRequest
and the WebSocketSession
), and can createAuthenticationPlugin.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
- ThePrincipal
that was returned with the last authentication (either initial or reevaluate).- Returns:
- an
AuthenticationPlugin.AuthenticationResult
, which you get from any of the method on theAuthenticationPlugin.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-ClientSEND
orMatsSocketServer.request(String, String, String, Object, String, String, byte[])
REQUEST}. If the method returnsinvalidAuthentication()
, 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 theinitialAuthentication
, 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 invokereevaluateAuthentication(..)
, 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 toreevaluateAuthentication
, getinvalidAuthentication()
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 thisSessionAuthenticator
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: ThisSessionAuthenticator
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 thisSessionAuthenticator
was happy with this exact Authorization value as parameter 'lastAuthenticatedTimestamp' - that is, this timestamp is reset each timeinitialAuthentication(..)
andreevaluateAuthentication
answers any ofauthenticated
orAuthenticationPlugin.AuthenticationContext.stillValid()
. An ok implementation is therefore to simply answerstillValid()
if the 'lastAuthenticatedTimestamp' parameter is less than X minutes ago, otherwise answerinvalidAuthentication()
(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 inreevaluateAuthentication(..)
, 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 (theHandshakeRequest
and the WebSocketSession
), and can createAuthenticationPlugin.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
- ThePrincipal
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 sameSessionAuthenticator
.- Returns:
- an
AuthenticationPlugin.AuthenticationResult
, which you get from any of the method on theAuthenticationPlugin.AuthenticationContext
- preferably eitherstillValid()
orinvalidAuthentication()
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 suppliedAuthenticationPlugin.AuthenticationContext
. The default implementation returntrue
, i.e. letting all users subscribe to all Topics.- Parameters:
context
- theAuthenticationPlugin.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.
-