[ws] implement jwt token authorization

parent 200f3b52
Pipeline #6140 failed with stages
in 82 minutes and 23 seconds
...@@ -48,7 +48,7 @@ data CheckAuth = InvalidUser | InvalidPassword | Valid Token TreeId UserId ...@@ -48,7 +48,7 @@ data CheckAuth = InvalidUser | InvalidPassword | Valid Token TreeId UserId
data AuthenticatedUser = AuthenticatedUser data AuthenticatedUser = AuthenticatedUser
{ _auth_node_id :: NodeId { _auth_node_id :: NodeId
, _auth_user_id :: UserId , _auth_user_id :: UserId
} deriving (Generic) } deriving (Generic, Show, Eq)
makeLenses ''AuthenticatedUser makeLenses ''AuthenticatedUser
......
...@@ -205,7 +205,7 @@ newEnv logger port file = do ...@@ -205,7 +205,7 @@ newEnv logger port file = do
!central_exchange <- forkIO CE.gServer !central_exchange <- forkIO CE.gServer
!dispatcher <- D.dispatcher !dispatcher <- D.dispatcher settings'
{- An 'Env' by default doesn't have strict fields, but when constructing one in production {- An 'Env' by default doesn't have strict fields, but when constructing one in production
we want to force them to WHNF to avoid accumulating unnecessary thunks. we want to force them to WHNF to avoid accumulating unnecessary thunks.
......
...@@ -26,6 +26,9 @@ import Data.Aeson.Types (prependFailure, typeMismatch) ...@@ -26,6 +26,9 @@ import Data.Aeson.Types (prependFailure, typeMismatch)
import Data.ByteString.Char8 qualified as C import Data.ByteString.Char8 qualified as C
import Data.ByteString.Lazy qualified as BSL import Data.ByteString.Lazy qualified as BSL
import Data.List (nubBy) import Data.List (nubBy)
import Data.UUID.V4 as UUID
import Gargantext.API.Admin.Auth.Types (AuthenticatedUser(_auth_user_id))
import Gargantext.API.Admin.Types (jwtSettings, Settings, jwtSettings)
import Gargantext.Core.AsyncUpdates.CentralExchange.Types qualified as CETypes import Gargantext.Core.AsyncUpdates.CentralExchange.Types qualified as CETypes
import Gargantext.Core.Types (NodeId, UserId) import Gargantext.Core.Types (NodeId, UserId)
import Gargantext.Prelude import Gargantext.Prelude
...@@ -34,6 +37,7 @@ import Nanomsg ...@@ -34,6 +37,7 @@ import Nanomsg
import Network.WebSockets qualified as WS import Network.WebSockets qualified as WS
import Servant import Servant
import Servant.API.WebSocket qualified as WS import Servant.API.WebSocket qualified as WS
import Servant.Auth.Server (verifyJWT)
{- {-
...@@ -101,6 +105,9 @@ showSub sub = ...@@ -101,6 +105,9 @@ showSub sub =
subKey :: Subscription -> ByteString subKey :: Subscription -> ByteString
subKey sub = wsKey $ s_ws_key_connection sub subKey sub = wsKey $ s_ws_key_connection sub
type Token = Text
{- {-
We accept requests for subscription/unsubscription via websocket. We accept requests for subscription/unsubscription via websocket.
...@@ -115,6 +122,8 @@ browser. ...@@ -115,6 +122,8 @@ browser.
data WSRequest = data WSRequest =
WSSubscribe Topic WSSubscribe Topic
| WSUnsubscribe Topic | WSUnsubscribe Topic
| WSAuthorize Token
| WSDeauthorize
| WSPing | WSPing
| WSPong | WSPong
deriving (Eq, Show) deriving (Eq, Show)
...@@ -128,6 +137,10 @@ instance FromJSON WSRequest where ...@@ -128,6 +137,10 @@ instance FromJSON WSRequest where
"unsubscribe" -> do "unsubscribe" -> do
topic <- o .: "topic" topic <- o .: "topic"
pure $ WSUnsubscribe topic pure $ WSUnsubscribe topic
"authorize" -> do
token <- o .: "token"
pure $ WSAuthorize token
"deauthorize" -> pure $ WSDeauthorize
"ping" -> pure WSPing "ping" -> pure WSPing
"pong" -> pure WSPong "pong" -> pure WSPong
s -> prependFailure "parsing request type failed, " (typeMismatch "request" s) s -> prependFailure "parsing request type failed, " (typeMismatch "request" s)
...@@ -139,11 +152,11 @@ data Dispatcher = ...@@ -139,11 +152,11 @@ data Dispatcher =
} }
dispatcher :: IO Dispatcher dispatcher :: Settings -> IO Dispatcher
dispatcher = do dispatcher authSettings = do
subscriptions <- newTVarIO ([] :: [Subscription]) subscriptions <- newTVarIO ([] :: [Subscription])
let server = wsServer subscriptions let server = wsServer authSettings subscriptions
d_ce_listener <- forkIO (ce_listener subscriptions) d_ce_listener <- forkIO (ce_listener subscriptions)
...@@ -181,8 +194,8 @@ removeSubscriptionsForWSKey subscriptions ws = ...@@ -181,8 +194,8 @@ removeSubscriptionsForWSKey subscriptions ws =
type WSAPI = "ws" :> WS.WebSocketPending type WSAPI = "ws" :> WS.WebSocketPending
wsServer :: TVar [Subscription] -> Server WSAPI wsServer :: Settings -> TVar [Subscription] -> Server WSAPI
wsServer subscriptions = streamData wsServer authSettings subscriptions = streamData
where where
streamData :: MonadIO m => WS.PendingConnection -> m () streamData :: MonadIO m => WS.PendingConnection -> m ()
streamData pc = do streamData pc = do
...@@ -191,7 +204,11 @@ wsServer subscriptions = streamData ...@@ -191,7 +204,11 @@ wsServer subscriptions = streamData
-- some unique, Sec-WebSocket-Key string. We use this to compare -- some unique, Sec-WebSocket-Key string. We use this to compare
-- connections (WS.Connection doesn't implement an Eq instance). -- connections (WS.Connection doesn't implement an Eq instance).
let mKey = head $ filter (\(k, _) -> k == "Sec-WebSocket-Key") $ WS.requestHeaders reqHead let mKey = head $ filter (\(k, _) -> k == "Sec-WebSocket-Key") $ WS.requestHeaders reqHead
let key = snd $ fromMaybe (panicTrace "Sec-WebSocket-Key not found!") mKey let key' = snd $ fromMaybe (panicTrace "Sec-WebSocket-Key not found!") mKey
-- Unfortunately, a single browsers sends the same
-- Sec-WebSocket-Key so we want to make that even more unique.
uuid <- liftIO $ UUID.nextRandom
let key = key' <> "-" <> show uuid
putText $ show $ WS.requestHeaders reqHead putText $ show $ WS.requestHeaders reqHead
c <- liftIO $ WS.acceptRequest pc c <- liftIO $ WS.acceptRequest pc
let ws = WSKeyConnection (key, c) let ws = WSKeyConnection (key, c)
...@@ -206,19 +223,26 @@ wsServer subscriptions = streamData ...@@ -206,19 +223,26 @@ wsServer subscriptions = streamData
wsLoop ws = flip finally disconnect $ do wsLoop ws = flip finally disconnect $ do
putText "[wsLoop] connecting" putText "[wsLoop] connecting"
forever $ do wsLoop' CUPublic
where
wsLoop' user = do
dm <- WS.receiveDataMessage (wsConn ws) dm <- WS.receiveDataMessage (wsConn ws)
case dm of
newUser <- case dm of
WS.Text dm' _ -> do WS.Text dm' _ -> do
case Aeson.decode dm' of case Aeson.decode dm' of
Nothing -> putText "[wsLoop] unknown message" Nothing -> do
putText "[wsLoop] unknown message"
return user
Just (WSSubscribe topic) -> do Just (WSSubscribe topic) -> do
-- TODO Fix s_connected_user based on header -- TODO Fix s_connected_user based on header
let sub = Subscription { s_connected_user = CUPublic let sub = Subscription { s_connected_user = user
, s_ws_key_connection = ws , s_ws_key_connection = ws
, s_topic = topic } , s_topic = topic }
ss <- insertSubscription subscriptions sub ss <- insertSubscription subscriptions sub
putText $ "[wsLoop] subscriptions: " <> show (showSub <$> ss) putText $ "[wsLoop] subscriptions: " <> show (showSub <$> ss)
return user
Just (WSUnsubscribe topic) -> do Just (WSUnsubscribe topic) -> do
-- TODO Fix s_connected_user based on header -- TODO Fix s_connected_user based on header
let sub = Subscription { s_connected_user = CUPublic let sub = Subscription { s_connected_user = CUPublic
...@@ -226,12 +250,31 @@ wsServer subscriptions = streamData ...@@ -226,12 +250,31 @@ wsServer subscriptions = streamData
, s_topic = topic } , s_topic = topic }
ss <- removeSubscription subscriptions sub ss <- removeSubscription subscriptions sub
putText $ "[wsLoop] subscriptions: " <> show (showSub <$> ss) putText $ "[wsLoop] subscriptions: " <> show (showSub <$> ss)
return user
Just (WSAuthorize token) -> do
let jwtS = authSettings ^. jwtSettings
mUser <- liftBase $ verifyJWT jwtS (encodeUtf8 token)
putText $ "[wsLoop] authorized user: " <> show mUser
-- TODO Update my subscriptions!
return $ fromMaybe user (CUUser . _auth_user_id <$> mUser)
Just WSDeauthorize -> do
-- TODO Update my subscriptions!
pure CUPublic
Just WSPing -> do Just WSPing -> do
WS.sendDataMessage (wsConn ws) (WS.Text (Aeson.encode Pong) Nothing) WS.sendDataMessage (wsConn ws) (WS.Text (Aeson.encode Pong) Nothing)
return user
Just WSPong -> do Just WSPong -> do
putText $ "[wsLoop] pong received" putText $ "[wsLoop] pong received"
_ -> putText "[wsLoop] binary ws messages not supported" return user
where _ -> do
putText "[wsLoop] binary ws messages not supported"
return user
wsLoop' newUser
disconnect = do disconnect = do
putText "[wsLoop] disconnecting..." putText "[wsLoop] disconnecting..."
ss <- removeSubscriptionsForWSKey subscriptions ws ss <- removeSubscriptionsForWSKey subscriptions ws
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment