packages feed

shomei-servant-0.2.0.0: src/Shomei/Session/Handler.hs

-- | Session and administrative-session HTTP adapters.
module Shomei.Session.Handler
  ( sessionServer,
    adminSessionServer,
    clientIpText,
  )
where

import Network.Socket (SockAddr)
import Servant (Handler)
import Servant.Server.Generic (AsServerT)
import Shomei.Account.Admin.Workflow qualified as Admin
import Shomei.Account.Handler (requireExistingUser)
import Shomei.Account.LoginId.Domain (loginIdText, mkLoginId)
import Shomei.Account.Password.Domain (PlainPassword (..))
import Shomei.Config (CookieConfig (..), ShomeiConfig (..), transportUsesCookies)
import Shomei.Delegation.Handler (denyUnderDelegation)
import Shomei.Error (AuthError (SessionNotFound))
import Shomei.Id (SessionId, UserId)
import Shomei.Prelude
import Shomei.Servant.Application (port, rejectAuth, rejectProblem, runApplicationHandler, workflow)
import Shomei.Servant.Auth (AuthUser (..), originHeaderAllowed)
import Shomei.Servant.ClientIp (clientIpText)
import Shomei.Servant.Cookie
  ( clearedCookies,
    refreshTokenFromCookie,
    tokenCookies,
  )
import Shomei.Servant.Error
  ( detailOccurrence,
    noProblemOccurrence,
    pcBadRequest,
    pcCsrfRejected,
    pcSessionNotFound,
  )
import Shomei.Servant.Result (CookieResponse (..), cookieResponse)
import Shomei.Servant.Seam (Env (..))
import Shomei.Session.Admin.Api (AdminSessionApi (..))
import Shomei.Session.Api (SessionApi (..))
import Shomei.Session.Authentication.Workflow qualified as Authentication
import Shomei.Session.Command
  ( ClientContext (..),
    LoginCommand (..),
    LogoutCommand (..),
    RefreshCommand (..),
  )
import Shomei.Session.Dto
import Shomei.Session.LoginAttempt.Domain (ClientIp (..))
import Shomei.Session.RefreshToken.Domain (RefreshToken (..))
import Shomei.Session.Result
import Shomei.Session.Store (findSessionById, listSessionsForUser)

sessionServer :: Env -> SessionApi (AsServerT Handler)
sessionServer env =
  SessionApi
    { login = loginH env,
      refresh = refreshH env,
      logout = logoutH env,
      currentSession = currentSessionH env
    }

adminSessionServer :: Env -> AdminSessionApi (AsServerT Handler)
adminSessionServer env =
  AdminSessionApi
    { listSessions = adminListSessionsH env,
      revokeSessions = adminRevokeSessionsH env,
      revokeSession = adminRevokeSessionH env
    }

loginH :: Env -> SockAddr -> LoginRequest -> Handler LoginResult
loginH env peer request = runApplicationHandler do
  loginId <- either rejectAuth pure (mkLoginId request.loginId)
  let command = LoginCommand {loginId, password = PlainPassword request.password}
      context =
        ClientContext
          { clientIp = ClientIp (clientIpText peer),
            accountKey = env.accountKeyOf (loginIdText loginId)
          }
  result <- workflow env (Authentication.login env.config context command)
  pure case result of
    Authentication.LoginComplete _ pair ->
      cookieResponse env.config (tokenCookies env.config pair) (loginResultToResponse env.config result)
    Authentication.MfaRequired _ ->
      CookieResponse
        { cookieBody = loginResultToResponse env.config result,
          sessionCookieHeader = Nothing,
          refreshCookieHeader = Nothing
        }

refreshH :: Env -> Maybe Text -> Maybe Text -> Maybe Text -> RefreshRequest -> Handler RefreshResult
refreshH env cookieHeader origin referer request = runApplicationHandler do
  presented <- case request.refreshToken of
    Just token -> pure token
    Nothing
      | transportUsesCookies env.config.tokenTransport,
        Just raw <- cookieHeader,
        Just token <- refreshTokenFromCookie env.config.cookieConfig raw -> do
          unless (originHeaderAllowed env.config.cookieConfig.allowedOrigins origin referer) (rejectProblem pcCsrfRejected noProblemOccurrence)
          pure token
    Nothing -> rejectProblem pcBadRequest (detailOccurrence "refreshToken required")
  pair <- workflow env (Authentication.refresh env.config (RefreshCommand (RefreshToken presented)))
  pure (cookieResponse env.config (tokenCookies env.config pair) (tokenPairToResponse env.config pair))

logoutH :: Env -> AuthUser -> Handler LogoutResult
logoutH env user = runApplicationHandler do
  outcome <- port env (Authentication.logout env.config (LogoutCommand user.authSessionId))
  case outcome of
    Left SessionNotFound -> pure cleared
    Left err -> rejectAuth err
    Right () -> pure cleared
  where
    cleared = cookieResponse env.config (clearedCookies env.config) ()

currentSessionH :: Env -> AuthUser -> Handler CurrentSessionResult
currentSessionH env user = runApplicationHandler do
  found <- port env (findSessionById user.authSessionId)
  maybe (rejectProblem pcSessionNotFound noProblemOccurrence) (pure . sessionToResponse) found

adminListSessionsH :: Env -> AuthUser -> UserId -> Handler ListSessionsResult
adminListSessionsH env _ target = runApplicationHandler do
  _ <- requireExistingUser env target
  map sessionToResponse <$> port env (listSessionsForUser target)

adminRevokeSessionsH :: Env -> AuthUser -> UserId -> Handler RevokeSessionsResult
adminRevokeSessionsH env actor target = runApplicationHandler do
  denyUnderDelegation env "admin_revoke_sessions" actor
  _ <- requireExistingUser env target
  void $ workflow env (Admin.revokeUserSessions actor.authUserId target)

adminRevokeSessionH :: Env -> AuthUser -> SessionId -> Handler RevokeSessionResult
adminRevokeSessionH env actor sessionId = runApplicationHandler do
  denyUnderDelegation env "admin_revoke_session" actor
  workflow env (Admin.revokeOneSession actor.authUserId sessionId)