never executed always true always false
1 module PureClaw.Gateway.Auth
2 ( -- * Authentication
3 authenticateRequest
4 , AuthError (..)
5 -- * Pairing
6 , handlePairRequest
7 , PairRequest (..)
8 , PairResponse (..)
9 ) where
10
11 import Data.Aeson
12 import Data.ByteString (ByteString)
13 import Data.Text (Text)
14 import Data.Text.Encoding qualified as TE
15 import Data.ByteString.Base16 qualified as Base16
16 import Data.ByteString qualified as BS
17
18 import PureClaw.Core.Errors
19 import PureClaw.Handles.Log
20 import PureClaw.Security.Pairing
21 import PureClaw.Security.Secrets
22
23 -- | Authentication errors.
24 data AuthError
25 = MissingToken
26 | InvalidToken
27 | MalformedHeader
28 deriving stock (Show, Eq)
29
30 instance ToPublicError AuthError where
31 toPublicError _ = NotAllowedError
32
33 -- | Extract and validate a bearer token from an Authorization header value.
34 -- Expects the format: "Bearer <token>"
35 authenticateRequest :: PairingState -> ByteString -> LogHandle -> IO (Either AuthError ())
36 authenticateRequest ps authHeader lh =
37 case extractToken authHeader of
38 Nothing -> do
39 _lh_logWarn lh "Auth: malformed Authorization header"
40 pure (Left MalformedHeader)
41 Just tokenBytes -> do
42 let rawBytes = Base16.decodeLenient tokenBytes
43 token = mkBearerToken rawBytes
44 valid <- verifyToken ps token
45 if valid
46 then pure (Right ())
47 else do
48 _lh_logWarn lh "Auth: invalid bearer token"
49 pure (Left InvalidToken)
50
51 -- | Extract the raw token bytes from a "Bearer <token>" header value.
52 extractToken :: ByteString -> Maybe ByteString
53 extractToken header =
54 let prefix = "Bearer "
55 in if BS.isPrefixOf prefix header
56 then Just (BS.drop (BS.length prefix) header)
57 else Nothing
58
59 -- | Request body for the /pair endpoint.
60 newtype PairRequest = PairRequest
61 { _pr_code :: Text
62 }
63 deriving stock (Show, Eq)
64
65 instance FromJSON PairRequest where
66 parseJSON = withObject "PairRequest" $ \o ->
67 PairRequest <$> o .: "code"
68
69 -- | Response body for a successful pairing.
70 newtype PairResponse = PairResponse
71 { _prsp_token :: Text
72 }
73 deriving stock (Show, Eq)
74
75 instance ToJSON PairResponse where
76 toJSON pr = object ["token" .= _prsp_token pr]
77
78 -- | Handle a pairing request: validate the code and return a bearer token.
79 handlePairRequest :: PairingState -> Text -> PairRequest -> LogHandle -> IO (Either PairingError PairResponse)
80 handlePairRequest ps clientId req lh = do
81 let code = mkPairingCode (_pr_code req)
82 _lh_logInfo lh $ "Pair: attempt from client " <> clientId
83 result <- attemptPair ps clientId code
84 case result of
85 Left err -> do
86 _lh_logWarn lh $ "Pair: failed for client " <> clientId
87 pure (Left err)
88 Right token ->
89 withBearerToken token $ \tokenBytes -> do
90 _lh_logInfo lh $ "Pair: success for client " <> clientId
91 pure (Right (PairResponse (TE.decodeUtf8 (Base16.encode tokenBytes))))