packages feed

natskell-1.0.0.1: internal/Policy/Auth/Resolver.hs

{-# LANGUAGE TypeApplications #-}

module Auth.Resolver
  ( applyAuthPatch
  , buildAuthPatch
  ) where

import           Auth.Credentials  (JwtBundle (..), parseJwtBundle)
import qualified Auth.NKey.Codec   as NKeyCodec
import           Auth.Types
import           Control.Exception
    ( SomeAsyncException
    , SomeException
    , displayException
    , fromException
    , throwIO
    , try
    )
import qualified Data.ByteString   as BS
import qualified Types.Connect     as Connect

buildAuthPatch :: Auth -> AuthContext -> IO (Either AuthError AuthPatch)
buildAuthPatch auth ctx = do
  tokenResult <- resolveOptional "auth token" validateToken (authTokenHandler auth)
  userPassResult <- resolveOptional "user/pass" validateUserPass (authUserPassHandler auth)
  challengeResult <- buildChallengePatch (authChallenge auth) ctx
  pure $ do
    token <- tokenResult
    userPass <- userPassResult
    challengePatch <- challengeResult
    pure challengePatch
      { patchAuthToken = token
      , patchUser = fst <$> userPass
      , patchPass = snd <$> userPass
      }

resolveOptional
  :: String
  -> (a -> Either AuthError ())
  -> Maybe (IO (Either AuthError a))
  -> IO (Either AuthError (Maybe a))
resolveOptional _ _ Nothing = pure (Right Nothing)
resolveOptional label validate (Just handler) = do
  result <- runAuthHandler label handler
  pure $ do
    value <- result
    validate value
    pure (Just value)

buildChallengePatch :: Maybe ChallengeAuth -> AuthContext -> IO (Either AuthError AuthPatch)
buildChallengePatch Nothing _ = pure (Right emptyAuthPatch)
buildChallengePatch (Just challenge) ctx =
  case requireNonce ctx of
    Left err -> pure (Left err)
    Right nonce ->
      case challenge of
        AuthNKeySeed seed ->
          pure $ do
            (publicKey, signature) <- signNonceWithSeedRaw seed nonce
            pure emptyAuthPatch
              { patchNKey = Just publicKey
              , patchSig = Just (NKeyCodec.encodeSignature signature)
              }
        AuthNKeyHandler publicKey signatureHandler ->
          case validatePublicKey publicKey of
            Left err -> pure (Left err)
            Right () -> do
              signatureResult <- resolveSignature signatureHandler nonce
              pure $ do
                signature <- signatureResult
                pure emptyAuthPatch
                  { patchNKey = Just publicKey
                  , patchSig = Just signature
                  }
        AuthJWTBundle creds ->
          pure $ do
            bundle <-
              maybe (Left (AuthError "jwt credentials bundle is invalid")) Right (parseJwtBundle creds)
            (_, signature) <- signNonceWithSeedRaw (jwtSeed bundle) nonce
            pure emptyAuthPatch
              { patchJwt = Just (jwtToken bundle)
              , patchSig = Just (NKeyCodec.encodeSignature signature)
              }
        AuthJWTHandlers jwtHandler signatureHandler -> do
          jwtResult <- runAuthHandler "jwt" jwtHandler
          case jwtResult >>= validateRawJwtValue of
            Left err -> pure (Left err)
            Right jwt -> do
              signatureResult <- resolveSignature signatureHandler nonce
              pure $ do
                signature <- signatureResult
                pure emptyAuthPatch
                  { patchJwt = Just jwt
                  , patchSig = Just signature
                  }

signNonceWithSeedRaw
  :: BS.ByteString
  -> BS.ByteString
  -> Either AuthError (BS.ByteString, BS.ByteString)
signNonceWithSeedRaw seed nonce =
  either (Left . AuthError) Right (NKeyCodec.signNonceWithSeedRaw seed nonce)

resolveSignature :: SignatureHandler -> Nonce -> IO (Either AuthError BS.ByteString)
resolveSignature handler nonce = do
  result <- runAuthHandler "nonce signature" (handler nonce)
  pure $ do
    signature <- result
    if BS.length signature == 64
      then Right (NKeyCodec.encodeSignature signature)
      else Left (AuthError "nonce signature must contain 64 raw bytes")

runAuthHandler :: String -> IO (Either AuthError a) -> IO (Either AuthError a)
runAuthHandler label action = do
  result <- try @SomeException action
  case result of
    Right value -> pure value
    Left err ->
      case fromException err :: Maybe SomeAsyncException of
        Just _  -> throwIO err
        Nothing -> pure (Left (AuthError (label ++ " handler failed: " ++ displayException err)))

applyAuthPatch :: AuthPatch -> Connect.Connect -> Connect.Connect
applyAuthPatch patch connect =
  connect
    { Connect.auth_token = patchAuthToken patch
    , Connect.user = patchUser patch
    , Connect.pass = patchPass patch
    , Connect.jwt = patchJwt patch
    , Connect.nkey = patchNKey patch
    , Connect.sig = patchSig patch
    }

validateToken :: AuthTokenData -> Either AuthError ()
validateToken token
  | BS.null token = Left (AuthError "auth token must not be empty")
  | otherwise = Right ()

validateUserPass :: UserPassData -> Either AuthError ()
validateUserPass (user, pass)
  | BS.null user = Left (AuthError "user must not be empty")
  | BS.null pass = Left (AuthError "pass must not be empty")
  | otherwise = Right ()

validatePublicKey :: NKeyPublicKey -> Either AuthError ()
validatePublicKey =
  either (Left . AuthError . ("nkey public key is invalid: " ++)) Right
    . NKeyCodec.validateUserPublicKey

validateRawJwtValue :: JWTTokenData -> Either AuthError JWTTokenData
validateRawJwtValue token
  | BS.null token = Left (AuthError "jwt must not be empty")
  | otherwise = Right token

requireNonce :: AuthContext -> Either AuthError BS.ByteString
requireNonce (AuthContext maybeNonce) =
  maybe (Left (AuthError "auth method requires a server nonce")) Right maybeNonce