From 7b73f9815ecf3218a10825e46422243b7c729f3b Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 10 Jan 2024 14:49:37 +0000 Subject: [PATCH 1/6] updated rusty-jwt-tools to v0.8.0 --- nix/pkgs/rusty_jwt_tools_ffi/default.nix | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/nix/pkgs/rusty_jwt_tools_ffi/default.nix b/nix/pkgs/rusty_jwt_tools_ffi/default.nix index 9d664c6bd5..ee6218a357 100644 --- a/nix/pkgs/rusty_jwt_tools_ffi/default.nix +++ b/nix/pkgs/rusty_jwt_tools_ffi/default.nix @@ -10,14 +10,14 @@ # Cargo.lock file in its root (not at the ffi/ subpath). let - version = "0.5.0"; + version = "0.8.0"; src = fetchFromGitHub { owner = "wireapp"; repo = "rusty-jwt-tools"; - rev = "6704e08376bb49168133d8f4ce66155adeb6bfb0"; - sha256 = "sha256-ocmeFXjU3psCO+hpDuEAIzYIm4QzP+jHJR/V8yyw6Lw="; + rev = "064d531b6f0d0b502755dceb3ab73f0f9ad02143"; + sha256 = "sha256-OqL4ue6swci3JqQKNmzcvpGxQAMhF8bHTXMp6dvIn9o="; }; - cargoLockFile = builtins.toFile "cargo.lock" (builtins.readFile "${src}/ffi/Cargo.lock"); + cargoLockFile = builtins.toFile "cargo.lock" (builtins.readFile "${src}/Cargo.lock"); in rustPlatform.buildRustPackage { @@ -29,7 +29,12 @@ rustPlatform.buildRustPackage { outputHashes = { # if any of these need updating, replace / create new key with # lib.fakeSha256, rebuild, and replace with actual hash. - "jwt-simple-0.11.4" = "sha256-zLKEvL6M7WD7F7HIABqq4b2rmlCS88QXDsj4JhAPe7o="; + "biscuit-0.6.0-beta1" = "sha256-no7b4Un+7AES7EwWdZh/oeIa4w0caKLAUFsHWqgJOrg="; + "certval-0.1.4" = "sha256-mUg3Kx1I/r9zBoB7tDaZsykFkE+tsN+Rem6DjUOZbuU="; + "jwt-simple-0.12.1" = "sha256-5PAOwulL8j6f4Ycoa5Q+1dqEA24uN8rJt+i2RebL6eo="; + "rcgen-0.9.2" = "sha256-3jFzInwdzFBot+L2Vm5NLF1ml33GH2+Iv3LqqGhLxFs="; + "ring-0.17.0-not-released-yet" = "sha256-TP8yZo64J/d1fw8l2J4+ol70EcHvpvHJBdpF3A+6Dgo="; + "x509-ocsp-0.2.1" = "sha256-Tdswn977QtS+i69q82dF/nkXIblUaCsqPD2SqUIYLWc="; }; }; From d624fd7cb8e6dc94fa7a9658dbb355169b24091d Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 11 Jan 2024 15:22:09 +0000 Subject: [PATCH 2/6] adapt create access token handler and tests --- libs/jwt-tools/src/Data/Jwt/Tools.hs | 36 ++++++++++++++++--- libs/jwt-tools/test/Spec.hs | 33 +++++++---------- services/brig/src/Brig/API/Client.hs | 23 +++++++++--- services/brig/src/Brig/API/Error.hs | 4 +++ services/brig/src/Brig/API/Types.hs | 2 ++ services/brig/src/Brig/Effects/JwtTools.hs | 8 ++++- .../brig/test/integration/API/User/Client.hs | 27 ++++++++++---- 7 files changed, 96 insertions(+), 37 deletions(-) diff --git a/libs/jwt-tools/src/Data/Jwt/Tools.hs b/libs/jwt-tools/src/Data/Jwt/Tools.hs index 4ccf6cf571..053bd82a9f 100644 --- a/libs/jwt-tools/src/Data/Jwt/Tools.hs +++ b/libs/jwt-tools/src/Data/Jwt/Tools.hs @@ -33,10 +33,12 @@ module Data.Jwt.Tools ExpiryEpoch (..), NowEpoch (..), PemBundle (..), + Handle (..), + TeamId (..), ) where -import Control.Exception +import Control.Exception hiding (handle) import Control.Monad.Trans.Except import Data.ByteString.Conversion import Foreign.C.String (CString, newCString, peekCString) @@ -50,6 +52,10 @@ type ProofCStr = CString type UserIdCStr = CString +type TeamIdCStr = CString + +type HandleCStr = CString + type ClientIdWord64 = Word64 type DomainCStr = CString @@ -73,6 +79,8 @@ foreign import ccall unsafe "generate_dpop_access_token" ProofCStr -> UserIdCStr -> ClientIdWord64 -> + HandleCStr -> + TeamIdCStr -> DomainCStr -> NonceCStr -> UrlCStr -> @@ -93,6 +101,8 @@ generateDpopAccessTokenFfi :: ProofCStr -> UserIdCStr -> ClientIdWord64 -> + HandleCStr -> + TeamIdCStr -> DomainCStr -> NonceCStr -> UrlCStr -> @@ -102,8 +112,8 @@ generateDpopAccessTokenFfi :: EpochWord64 -> BackendBundleCStr -> IO (Maybe (Ptr HsResult)) -generateDpopAccessTokenFfi dpopProof user client domain nonce uri method maxSkewSecs expiration now backendKeys = do - ptr <- generate_dpop_access_token dpopProof user client domain nonce uri method maxSkewSecs expiration now backendKeys +generateDpopAccessTokenFfi dpopProof user client handle tid domain nonce uri method maxSkewSecs expiration now backendKeys = do + ptr <- generate_dpop_access_token dpopProof user client handle tid domain nonce uri method maxSkewSecs expiration now backendKeys if ptr /= nullPtr then pure $ Just ptr else pure Nothing @@ -127,6 +137,8 @@ generateDpopToken :: Proof -> UserId -> ClientId -> + Handle -> + TeamId -> Domain -> Nonce -> Uri -> @@ -136,9 +148,11 @@ generateDpopToken :: NowEpoch -> PemBundle -> ExceptT DPoPTokenGenerationError m ByteString -generateDpopToken dpopProof uid cid domain nonce uri method maxSkewSecs maxExpiration now backendPubkeyBundle = do +generateDpopToken dpopProof uid cid handle tid domain nonce uri method maxSkewSecs maxExpiration now backendPubkeyBundle = do dpopProofCStr <- toCStr dpopProof uidCStr <- toCStr uid + handleCStr <- toCStr handle + tidCStr <- toCStr tid domainCStr <- toCStr domain nonceCStr <- toCStr nonce uriCStr <- toCStr uri @@ -150,6 +164,8 @@ generateDpopToken dpopProof uid cid domain nonce uri method maxSkewSecs maxExpir dpopProofCStr uidCStr (_unClientId cid) + handleCStr + tidCStr domainCStr nonceCStr uriCStr @@ -213,6 +229,14 @@ newtype ClientId = ClientId {_unClientId :: Word64} deriving (Eq, Show) deriving newtype (ToByteString) +newtype Handle = Handle {_unHandle :: ByteString} + deriving (Eq, Show) + deriving newtype (ToByteString) + +newtype TeamId = TeamId {_unTeamId :: ByteString} + deriving (Eq, Show) + deriving newtype (ToByteString) + newtype Domain = Domain {_unDomain :: ByteString} deriving (Eq, Show) deriving newtype (ToByteString) @@ -323,4 +347,8 @@ data DPoPTokenGenerationError UnsupportedApiVersion | -- Bubbling up errors UnsupportedScope + | -- Client handle does not match the supplied handle + DpopHandleMismatch + | -- Client team does not match the supplied team + DpopTeamMismatch deriving (Eq, Show, Generic, Bounded, Enum) diff --git a/libs/jwt-tools/test/Spec.hs b/libs/jwt-tools/test/Spec.hs index a2881bc532..586bdc8386 100644 --- a/libs/jwt-tools/test/Spec.hs +++ b/libs/jwt-tools/test/Spec.hs @@ -24,20 +24,11 @@ main :: IO () main = hspec $ do describe "generateDpopToken FFI when passing valid inputs" $ do it "should return an access token" $ do - -- FUTUREWORK(leif): fix this test, we need new valid test data, - -- this test exists mainly for debugging purposes - -- a functionality test is also coverd in the integration tests in services/brig/test/integration/API/User/Client.hs (`testCreateAccessToken`) - pending - actual <- runExceptT $ generateDpopToken proof uid cid domain nonce uri method maxSkewSecs expires now pem - print actual + actual <- runExceptT $ generateDpopToken proof uid cid handle tid domain nonce uri method maxSkewSecs expires now pem isRight actual `shouldBe` True describe "generateDpopToken FFI when passing a wrong nonce value" $ do it "should return BackendNonceMismatchError" $ do - -- FUTUREWORK(leif): fix this test, we need new valid test data, - -- this test exists mainly for debugging purposes - -- a functionality test is also coverd in the integration tests in services/brig/test/integration/API/User/Client.hs (`testCreateAccessToken`) - pending - actual <- runExceptT $ generateDpopToken proof uid cid domain (Nonce "foobar") uri method maxSkewSecs expires now pem + actual <- runExceptT $ generateDpopToken proof uid cid handle tid domain (Nonce "foobar") uri method maxSkewSecs expires now pem actual `shouldBe` Left BackendNonceMismatchError describe "toResult" $ do it "should convert to correct error" $ do @@ -81,16 +72,16 @@ main = hspec $ do toResult Nothing Nothing `shouldBe` Left UnknownError where token = "" - proof = Proof "eyJhbGciOiJFZERTQSIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoidXE2c1hXcDdUM1E3YlNtUFd3eFNlRHJoUHFid1RfcTd4SFBQeGpGT0g5VSJ9fQ.eyJpYXQiOjE2OTQxMTc0MjgsImV4cCI6MTY5NDcyMjIyOCwibmJmIjoxNjk0MTE3NDIzLCJzdWIiOiJpbTp3aXJlYXBwPUlHOVl2enVXUUlLVWFSazEyRjVDSVEvOGUxODk2MjZlYWUwMTExZEBlbG5hLndpcmUubGluayIsImp0aSI6ImM0OGZmOTAyLTc5OGEtNDNjYi04YTk2LTE3NzM0NTgxNjIyMCIsIm5vbmNlIjoiR0FxNG5SajlSWVNzUnhoOVh1MWFtQSIsImh0bSI6IlBPU1QiLCJodHUiOiJodHRwczovL2VsbmEud2lyZS5saW5rL2NsaWVudHMvOGUxODk2MjZlYWUwMTExZC9hY2Nlc3MtdG9rZW4iLCJjaGFsIjoiMkxLbEFWMjR2VGtIMHlaaFdacEZrT01mSEE1d3lGQkgifQ.FW5i40CvndSSo3wQdA1DMUkGRmxk86cORAllwC2PCejVuk7TsdZuIKuJZFVa1VTJKWwNCPqPZ05Gsxxeh1DiDA" - uid = UserId "206f58bf-3b96-4082-9469-1935d85e4221" - cid = ClientId 10239098846720299293 - domain = Domain "wire.com" - nonce = Nonce "GAq4nRj9RYSsRxh9Xu1amA" - uri = Uri "https://elna.wire.link/clients/10239098846720299293/access-token" + proof = Proof "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im5MSkdOLU9hNkpzcTNLY2xaZ2dMbDdVdkFWZG1CMFE2QzNONUJDZ3BoSHcifSwidHlwIjoiZHBvcCtqd3QifQ.eyJjaGFsIjoid2EyVnJrQ3RXMXNhdUoyRDN1S1k4cmM3eTRrbDR1c0giLCJleHAiOjE4MzExMjYxNjMsImhhbmRsZSI6IndpcmVhcHA6Ly8lNDBwaHVoaGliZGhxYnF4cnpibnNhZndAZXhhbXBsZS5jb20iLCJodG0iOiJQT1NUIiwiaHR1IjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9jbGllbnRzL2NjNmU2NDBlMjk2ZThiYmEvYWNjZXNzLXRva2VuIiwiaWF0IjoxNzA0OTgyMTYzLCJqdGkiOiI2ZmM1OWU3Zi1iNjY2LTRmZmMtYjczOC00ZjQ3NjBjODg0Y2EiLCJuYmYiOjE3MDQ5ODIxNjMsIm5vbmNlIjoiVnZHYnc2ZVZUTkdTUWJLNVNlaVNiQSIsInN1YiI6IndpcmVhcHA6Ly9zZ3VNZUxKdFE2U3ZKUGNxUExiMkJnIWNjNmU2NDBlMjk2ZThiYmFAZXhhbXBsZS5jb20iLCJ0ZWFtIjoiNDAyNTE2ODAtMzVlMS00Mzc0LWIzYWEtNzU2MDBkZTc5ZTMzIn0.JgVXD2_E4j4sLcvD284Fj4z_6xmwA0czcP8wzHZmqPpel60HUqDVKDx5GmiWbFWix-E7ZXvYfvZ7NmxlDrgmAg" + uid = UserId "b20b8c78-b26d-43a4-af24-f72a3cb6f606" + cid = ClientId 14730821443162901434 + domain = Domain "example.com" + nonce = Nonce "VvGbw6eVTNGSQbK5SeiSbA" + uri = Uri "https://example.com/clients/cc6e640e296e8bba/access-token" method = POST - maxSkewSecs = MaxSkewSecs 5 - now = NowEpoch 360 - expires = ExpiryEpoch 2136351646 + maxSkewSecs = MaxSkewSecs 1 + now = NowEpoch 1704982162 + expires = ExpiryEpoch 1831212562 pem = PemBundle $ "-----BEGIN PRIVATE KEY-----\n\ @@ -99,3 +90,5 @@ main = hspec $ do \-----BEGIN PUBLIC KEY-----\n\ \MCowBQYDK2VwAyEAdYI38UdxksC0K4Qx6E9JK9YfGm+ehnY18oKmHL2YsZk=\n\ \-----END PUBLIC KEY-----\n" + handle = Handle "phuhhibdhqbqxrzbnsafw" + tid = TeamId "40251680-35e1-4374-b3aa-75600de79e33" diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 4ee96f6010..f7fb3a404c 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -75,10 +75,12 @@ import Brig.User.Email import Cassandra (MonadClient) import Control.Error import Control.Lens (view) +import Control.Monad.Trans.Except (except) import Data.ByteString.Conversion import Data.Code as Code import Data.Domain import Data.Either.Extra (mapLeft) +import Data.Handle (fromHandle) import Data.Id (ClientId, ConnId, UserId) import Data.List.Split (chunksOf) import Data.Map.Strict qualified as Map @@ -86,6 +88,7 @@ import Data.Misc (PlainTextPassword6) import Data.Qualified import Data.Set qualified as Set import Imports +import Network.HTTP.Types qualified as HTTP import Network.HTTP.Types.Method (StdMethod) import Network.Wai.Utilities import Polysemy (Member) @@ -492,22 +495,29 @@ createAccessToken :: createAccessToken luid cid method link proof = do let domain = tDomain luid let uid = tUnqualified luid + (tid, handle) <- do + mUser <- lift $ wrapClient (Data.lookupUser NoPendingInvitations uid) + except $ + (,) + <$> note NotATeamUser (userTeam =<< mUser) + <*> (urlEncode . fromHandle <$> (note MissingHandle (userHandle =<< mUser))) nonce <- ExceptT $ note NonceNotFound <$> wrapClient (Nonce.lookupAndDeleteNonce uid (cs $ toByteString cid)) - httpsUrl <- do - let urlBs = "https://" <> toByteString' domain <> "/" <> cs (toUrlPiece link) - maybe (throwE MisconfiguredRequestUrl) pure $ fromByteString $ urlBs + httpsUrl <- except $ note MisconfiguredRequestUrl $ fromByteString $ "https://" <> toByteString' domain <> "/" <> cs (toUrlPiece link) maxSkewSeconds <- Opt.setDpopMaxSkewSecs <$> view settings expiresIn <- Opt.setDpopTokenExpirationTimeSecs <$> view settings now <- fromUTCTime <$> lift (liftSem Now.get) let expiresAt = now & addToEpoch expiresIn - pathToKeys <- ExceptT $ note KeyBundleError . Opt.setPublicKeyBundle <$> view settings - pubKeyBundle <- ExceptT $ note KeyBundleError <$> liftSem (PublicKeyBundle.get pathToKeys) + pubKeyBundle <- do + pathToKeys <- ExceptT $ note KeyBundleError . Opt.setPublicKeyBundle <$> view settings + ExceptT $ note KeyBundleError <$> liftSem (PublicKeyBundle.get pathToKeys) token <- ExceptT $ liftSem $ JwtTools.generateDPoPAccessToken proof (ClientIdentity domain uid cid) + handle + tid nonce httpsUrl method @@ -516,3 +526,6 @@ createAccessToken luid cid method link proof = do now pubKeyBundle pure $ (DPoPAccessTokenResponse token DPoP expiresIn, NoStore) + where + urlEncode :: Text -> Text + urlEncode = cs . HTTP.urlEncode False . cs diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index d1d09ca01d..11786491a9 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -215,10 +215,14 @@ certEnrollmentError (RustError InvalidBackendKeys) = StdError $ Wai.mkError stat certEnrollmentError (RustError InvalidClientId) = StdError $ Wai.mkError status400 "invalid-client-id" "Bubbling up errors" certEnrollmentError (RustError UnsupportedApiVersion) = StdError $ Wai.mkError status400 "unsupported-api-version" "Bubbling up errors" certEnrollmentError (RustError UnsupportedScope) = StdError $ Wai.mkError status400 "unsupported-scope" "Bubbling up errors" +certEnrollmentError (RustError DpopHandleMismatch) = StdError $ Wai.mkError status400 "dpop-handle-mismatch" "Bubbling up errors" +certEnrollmentError (RustError DpopTeamMismatch) = StdError $ Wai.mkError status400 "dpop-team-mismatch" "Bubbling up errors" certEnrollmentError NonceNotFound = StdError $ Wai.mkError status400 "client-token-bad-nonce" "The client sent an unacceptable anti-replay nonce" certEnrollmentError MisconfiguredRequestUrl = StdError $ Wai.mkError status500 "misconfigured-request-url" "The request url cannot be derived from optSettings.setFederationDomain in brig.yaml" certEnrollmentError KeyBundleError = StdError $ Wai.mkError status404 "no-server-key-bundle" "The key bundle required for the certificate enrollment process could not be found" certEnrollmentError ClientIdSyntaxError = StdError $ Wai.mkError status400 "client-token-id-parse-error" "The client id could not be parsed" +certEnrollmentError NotATeamUser = StdError $ Wai.mkError status400 "not-a-team-user" "The user is not a team user" +certEnrollmentError MissingHandle = StdError $ Wai.mkError status400 "missing-handle" "The user has no handle" fedError :: FederationError -> Error fedError = StdError . federationErrorToWai diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index 8071bd7c4d..34fb803bc6 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -208,6 +208,8 @@ data CertEnrollmentError | KeyBundleError | MisconfiguredRequestUrl | ClientIdSyntaxError + | NotATeamUser + | MissingHandle ------------------------------------------------------------------------------- -- Exceptions diff --git a/services/brig/src/Brig/Effects/JwtTools.hs b/services/brig/src/Brig/Effects/JwtTools.hs index 036cf796d5..51d5450412 100644 --- a/services/brig/src/Brig/Effects/JwtTools.hs +++ b/services/brig/src/Brig/Effects/JwtTools.hs @@ -26,6 +26,10 @@ data JwtTools m a where Proof -> -- | The qualified client ID associated with the currently logged on user ClientIdentity -> + -- | The user's handle + Text -> + -- | The user's team ID + TeamId -> -- | The most recent DPoP nonce provided by the backend to the current client Nonce -> -- | The HTTPS URI on the backend for the DPoP auth token endpoint @@ -46,7 +50,7 @@ makeSem ''JwtTools interpretJwtTools :: Member (Embed IO) r => Sem (JwtTools ': r) a -> Sem r a interpretJwtTools = interpret $ \case - GenerateDPoPAccessToken pr ci n uri method skew ex now pem -> + GenerateDPoPAccessToken pr ci h t n uri method skew ex now pem -> mapLeft RustError <$> runExceptT ( DPoPAccessToken @@ -54,6 +58,8 @@ interpretJwtTools = interpret $ \case (Jwt.Proof (toByteString' pr)) (Jwt.UserId (toByteString' (ciUser ci))) (Jwt.ClientId (clientToWord64 (ciClient ci))) + (Jwt.Handle (toByteString' h)) + (Jwt.TeamId (toByteString' t)) (Jwt.Domain (toByteString' (ciDomain ci))) (Jwt.Nonce (toByteString' n)) (Jwt.Uri (toByteString' uri)) diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index 03cc98eb49..2dc80ba6e1 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -44,6 +44,7 @@ import Data.ByteString.Conversion import Data.Coerce (coerce) import Data.Default import Data.Domain (Domain (..)) +import Data.Handle import Data.Id import Data.List1 qualified as List1 import Data.Map qualified as Map @@ -56,6 +57,7 @@ import Data.Text.Encoding qualified as T import Data.Time (addUTCTime) import Data.Time.Clock.POSIX import Data.UUID (toByteString) +import Data.UUID qualified as UUID import Data.Vector qualified as Vec import Imports import Network.Wai.Utilities.Error qualified as Error @@ -1395,7 +1397,9 @@ data DPoPClaimsSet = DPoPClaimsSet claimNonce :: Text, claimHtm :: Text, claimHtu :: Text, - claimChal :: Text + claimChal :: Text, + claimHandle :: Text, + claimTeamId :: Text } deriving (Eq, Show, Generic) @@ -1410,6 +1414,8 @@ instance A.FromJSON DPoPClaimsSet where <*> o A..: "htm" <*> o A..: "htu" <*> o A..: "chal" + <*> o A..: "handle" + <*> o A..: "team" instance A.ToJSON DPoPClaimsSet where toJSON s = @@ -1417,6 +1423,8 @@ instance A.ToJSON DPoPClaimsSet where & ins "htm" (claimHtm s) & ins "htu" (claimHtu s) & ins "chal" (claimChal s) + & ins "handle" (claimHandle s) + & ins "team" (claimTeamId s) where ins k v (Object o) = Object $ M.insert k (A.toJSON v) o ins _ _ a = a @@ -1424,12 +1432,15 @@ instance A.ToJSON DPoPClaimsSet where testCreateAccessToken :: Opts.Opts -> Nginz -> Brig -> Http () testCreateAccessToken opts n brig = do let localDomain = opts ^. Opt.optionSettings & Opt.setFederationDomain - u <- randomUser brig + (u, tid) <- Util.createUserWithTeam' brig + handle <- do + Just h <- userHandle <$> Util.setRandomHandle brig u + pure $ "wireapp://%40" <> fromHandle h <> "@" <> cs (toByteString' localDomain) let uid = userId u + let Just email = userEmail u -- convert the user Id into 16 octets of binary and then base64url let uidBS = Data.UUID.toByteString (toUUID uid) let uidB64 = encodeBase64UrlUnpadded (cs uidBS) - let email = fromMaybe (error "invalid email") $ userEmail u rs <- login n (defEmailLogin email) PersistentCookie (floor <$> getPOSIXTime) - let clientIdentity = cs $ "im:wireapp=" <> cs (toText uidB64) <> "/" <> toByteString' cid <> "@" <> toByteString' localDomain + let clientIdentity = cs $ "wireapp://" <> cs (toText uidB64) <> "!" <> toByteString' cid <> "@" <> toByteString' localDomain let httpsUrl = cs $ "https://" <> toByteString' localDomain <> "/clients/" <> toByteString' cid <> "/access-token" - let expClaim = NumericDate (addUTCTime 10 now) + let expClaim = NumericDate $ addUTCTime 10 now let claimsSet' = emptyClaimsSet & claimIat ?~ NumericDate now @@ -1448,7 +1459,7 @@ testCreateAccessToken opts n brig = do & claimNbf ?~ NumericDate now & claimSub ?~ fromMaybe (error "invalid sub claim") ((clientIdentity :: Text) ^? stringOrUri) & claimJti ?~ "6fc59e7f-b666-4ffc-b738-4f4760c884ca" - let dpopClaims = DPoPClaimsSet claimsSet' nonceBs "POST" httpsUrl "wa2VrkCtW1sauJ2D3uKY8rc7y4kl4usH" + let dpopClaims = DPoPClaimsSet claimsSet' nonceBs "POST" httpsUrl "wa2VrkCtW1sauJ2D3uKY8rc7y4kl4usH" handle (UUID.toText (toUUID tid)) signedOrError <- fmap encodeCompact <$> liftIO (signAccessToken dpopClaims) case signedOrError of Left err -> liftIO $ assertFailure $ "failed to sign claims: " <> show err @@ -1490,7 +1501,9 @@ testCreateAccessTokenMissingProof brig = do testCreateAccessTokenNoNonce :: Brig -> Http () testCreateAccessTokenNoNonce brig = do - uid <- userId <$> randomUser brig + (u, _) <- Util.createUserWithTeam' brig + void $ Util.setRandomHandle brig u + let uid = userId u cid <- createClientForUser brig uid Util.createAccessToken brig uid "some_host_name" cid (Just $ Proof "xxxx.yyyy.zzzz") !!! do From 82f4edbad4d0b463199afc210717b6303abd5c0c Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 11 Jan 2024 15:25:24 +0000 Subject: [PATCH 3/6] changelog --- changelog.d/5-internal/WPB-6099 | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5-internal/WPB-6099 diff --git a/changelog.d/5-internal/WPB-6099 b/changelog.d/5-internal/WPB-6099 new file mode 100644 index 0000000000..445d224c92 --- /dev/null +++ b/changelog.d/5-internal/WPB-6099 @@ -0,0 +1 @@ +Version of rusty-jwt-tools bumped to v0.8.0 From 4ecd5daff075ed6f3acd3ec8847da5f421e57b72 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 12 Jan 2024 10:03:12 +0000 Subject: [PATCH 4/6] improve jwt-tools unit test --- libs/jwt-tools/jwt-tools.cabal | 3 ++- libs/jwt-tools/test/Spec.hs | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/libs/jwt-tools/jwt-tools.cabal b/libs/jwt-tools/jwt-tools.cabal index 12a29c322f..57f815466a 100644 --- a/libs/jwt-tools/jwt-tools.cabal +++ b/libs/jwt-tools/jwt-tools.cabal @@ -79,7 +79,8 @@ test-suite jwt-tools-tests main-is: Spec.hs type: exitcode-stdio-1.0 build-depends: - hspec + bytestring + , hspec , imports , jwt-tools , transformers diff --git a/libs/jwt-tools/test/Spec.hs b/libs/jwt-tools/test/Spec.hs index 586bdc8386..4409a6ba0a 100644 --- a/libs/jwt-tools/test/Spec.hs +++ b/libs/jwt-tools/test/Spec.hs @@ -18,14 +18,19 @@ import Control.Monad.Trans.Except import Data.Jwt.Tools import Imports +import Data.ByteString.Char8 (split) import Test.Hspec main :: IO () main = hspec $ do describe "generateDpopToken FFI when passing valid inputs" $ do - it "should return an access token" $ do + it "should return an access token with the correct header" $ do actual <- runExceptT $ generateDpopToken proof uid cid handle tid domain nonce uri method maxSkewSecs expires now pem - isRight actual `shouldBe` True + -- The actual payload of the DPoP token is not deterministic as it depends on the current time. + -- We therefore only check the header, because if the header is correct, it means the token creation was successful.s + let expectedHeader = "eyJhbGciOiJFZERTQSIsInR5cCI6ImF0K2p3dCIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6ImRZSTM4VWR4a3NDMEs0UXg2RTlKSzlZZkdtLWVoblkxOG9LbUhMMllzWmsifX0" + let actualHeader = either (const "") (head . split '.') actual + actualHeader `shouldBe` expectedHeader describe "generateDpopToken FFI when passing a wrong nonce value" $ do it "should return BackendNonceMismatchError" $ do actual <- runExceptT $ generateDpopToken proof uid cid handle tid domain (Nonce "foobar") uri method maxSkewSecs expires now pem From 017e3e3d063fbbacd7e409a209b23ab65b7adb82 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 12 Jan 2024 10:24:27 +0000 Subject: [PATCH 5/6] linter --- libs/jwt-tools/default.nix | 3 ++- libs/jwt-tools/test/Spec.hs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/jwt-tools/default.nix b/libs/jwt-tools/default.nix index 58d7084889..a6cdf09b24 100644 --- a/libs/jwt-tools/default.nix +++ b/libs/jwt-tools/default.nix @@ -4,6 +4,7 @@ # dependencies are added or removed. { mkDerivation , base +, bytestring , bytestring-conversion , gitignoreSource , hspec @@ -25,7 +26,7 @@ mkDerivation { transformers ]; librarySystemDepends = [ rusty_jwt_tools_ffi ]; - testHaskellDepends = [ hspec imports transformers ]; + testHaskellDepends = [ bytestring hspec imports transformers ]; description = "FFI to rusty-jwt-tools"; license = lib.licenses.agpl3Only; } diff --git a/libs/jwt-tools/test/Spec.hs b/libs/jwt-tools/test/Spec.hs index 4409a6ba0a..6bf863078d 100644 --- a/libs/jwt-tools/test/Spec.hs +++ b/libs/jwt-tools/test/Spec.hs @@ -16,9 +16,9 @@ -- with this program. If not, see . import Control.Monad.Trans.Except +import Data.ByteString.Char8 (split) import Data.Jwt.Tools import Imports -import Data.ByteString.Char8 (split) import Test.Hspec main :: IO () From 2dea4cac32002b94be3b955ab9a5d1da40fe9dc4 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 15 Jan 2024 09:43:57 +0000 Subject: [PATCH 6/6] adressing review comments --- services/brig/src/Brig/API/Client.hs | 7 +------ services/brig/src/Brig/Effects/JwtTools.hs | 23 +++++++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index f7fb3a404c..2e7c16c1bd 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -80,7 +80,6 @@ import Data.ByteString.Conversion import Data.Code as Code import Data.Domain import Data.Either.Extra (mapLeft) -import Data.Handle (fromHandle) import Data.Id (ClientId, ConnId, UserId) import Data.List.Split (chunksOf) import Data.Map.Strict qualified as Map @@ -88,7 +87,6 @@ import Data.Misc (PlainTextPassword6) import Data.Qualified import Data.Set qualified as Set import Imports -import Network.HTTP.Types qualified as HTTP import Network.HTTP.Types.Method (StdMethod) import Network.Wai.Utilities import Polysemy (Member) @@ -500,7 +498,7 @@ createAccessToken luid cid method link proof = do except $ (,) <$> note NotATeamUser (userTeam =<< mUser) - <*> (urlEncode . fromHandle <$> (note MissingHandle (userHandle =<< mUser))) + <*> note MissingHandle (userHandle =<< mUser) nonce <- ExceptT $ note NonceNotFound <$> wrapClient (Nonce.lookupAndDeleteNonce uid (cs $ toByteString cid)) httpsUrl <- except $ note MisconfiguredRequestUrl $ fromByteString $ "https://" <> toByteString' domain <> "/" <> cs (toUrlPiece link) maxSkewSeconds <- Opt.setDpopMaxSkewSecs <$> view settings @@ -526,6 +524,3 @@ createAccessToken luid cid method link proof = do now pubKeyBundle pure $ (DPoPAccessTokenResponse token DPoP expiresIn, NoStore) - where - urlEncode :: Text -> Text - urlEncode = cs . HTTP.urlEncode False . cs diff --git a/services/brig/src/Brig/Effects/JwtTools.hs b/services/brig/src/Brig/Effects/JwtTools.hs index 51d5450412..f31329c5aa 100644 --- a/services/brig/src/Brig/Effects/JwtTools.hs +++ b/services/brig/src/Brig/Effects/JwtTools.hs @@ -6,6 +6,7 @@ import Brig.API.Types (CertEnrollmentError (..)) import Control.Monad.Trans.Except import Data.ByteString.Conversion import Data.Either.Extra +import Data.Handle (Handle, fromHandle) import Data.Id import Data.Jwt.Tools qualified as Jwt import Data.Misc (HttpsUrl) @@ -13,6 +14,7 @@ import Data.Nonce (Nonce) import Data.PEMKeys import Imports import Network.HTTP.Types (StdMethod (..)) +import Network.HTTP.Types qualified as HTTP import Polysemy import Wire.API.MLS.Credential (ClientIdentity (..)) import Wire.API.MLS.Epoch (Epoch (..)) @@ -27,7 +29,7 @@ data JwtTools m a where -- | The qualified client ID associated with the currently logged on user ClientIdentity -> -- | The user's handle - Text -> + Handle -> -- | The user's team ID TeamId -> -- | The most recent DPoP nonce provided by the backend to the current client @@ -50,18 +52,18 @@ makeSem ''JwtTools interpretJwtTools :: Member (Embed IO) r => Sem (JwtTools ': r) a -> Sem r a interpretJwtTools = interpret $ \case - GenerateDPoPAccessToken pr ci h t n uri method skew ex now pem -> + GenerateDPoPAccessToken proof cid handle tid nonce uri method skew ex now pem -> mapLeft RustError <$> runExceptT ( DPoPAccessToken <$> Jwt.generateDpopToken - (Jwt.Proof (toByteString' pr)) - (Jwt.UserId (toByteString' (ciUser ci))) - (Jwt.ClientId (clientToWord64 (ciClient ci))) - (Jwt.Handle (toByteString' h)) - (Jwt.TeamId (toByteString' t)) - (Jwt.Domain (toByteString' (ciDomain ci))) - (Jwt.Nonce (toByteString' n)) + (Jwt.Proof (toByteString' proof)) + (Jwt.UserId (toByteString' (ciUser cid))) + (Jwt.ClientId (clientToWord64 (ciClient cid))) + (Jwt.Handle (toByteString' (urlEncode (fromHandle (handle))))) + (Jwt.TeamId (toByteString' tid)) + (Jwt.Domain (toByteString' (ciDomain cid))) + (Jwt.Nonce (toByteString' nonce)) (Jwt.Uri (toByteString' uri)) method (Jwt.MaxSkewSecs skew) @@ -69,3 +71,6 @@ interpretJwtTools = interpret $ \case (Jwt.NowEpoch (epochNumber now)) (Jwt.PemBundle (toByteString' pem)) ) + where + urlEncode :: Text -> Text + urlEncode = cs . HTTP.urlEncode False . cs