diff --git a/changelog.d/1-api-changes/token-client-id b/changelog.d/1-api-changes/token-client-id new file mode 100644 index 0000000000..c75a0c2f4f --- /dev/null +++ b/changelog.d/1-api-changes/token-client-id @@ -0,0 +1 @@ +The `/access` endpoint now takes an optional `client_id` query parameter. The first time it is provided, a new user token will be generated containing the given client ID. Successive invocations of `/access` will ignore the `client_id` parameter. Some endpoints can now potentially require a client ID as part of the access token. When trying to invoke them with an access token that does not contain a client ID, an authentication error will occur. diff --git a/charts/cannon/templates/conf/_nginx.conf.tpl b/charts/cannon/templates/conf/_nginx.conf.tpl index 305d685122..d93ec92680 100644 --- a/charts/cannon/templates/conf/_nginx.conf.tpl +++ b/charts/cannon/templates/conf/_nginx.conf.tpl @@ -327,6 +327,7 @@ http { proxy_set_header Z-Type $zauth_type; proxy_set_header Z-User $zauth_user; + proxy_set_header Z-Client $zauth_client; proxy_set_header Z-Connection $zauth_connection; proxy_set_header Z-Provider $zauth_provider; proxy_set_header Z-Bot $zauth_bot; diff --git a/charts/nginz/templates/conf/_nginx.conf.tpl b/charts/nginz/templates/conf/_nginx.conf.tpl index e6e6909447..12936dc51e 100644 --- a/charts/nginz/templates/conf/_nginx.conf.tpl +++ b/charts/nginz/templates/conf/_nginx.conf.tpl @@ -299,6 +299,7 @@ http { proxy_set_header Z-Type $zauth_type; proxy_set_header Z-User $zauth_user; + proxy_set_header Z-Client $zauth_client; proxy_set_header Z-Connection $zauth_connection; proxy_set_header Z-Provider $zauth_provider; proxy_set_header Z-Bot $zauth_bot; diff --git a/deploy/services-demo/conf/nginz/common_response.conf b/deploy/services-demo/conf/nginz/common_response.conf index 833f131bf6..1b8a947f43 100644 --- a/deploy/services-demo/conf/nginz/common_response.conf +++ b/deploy/services-demo/conf/nginz/common_response.conf @@ -8,6 +8,7 @@ proxy_set_header Connection ""; proxy_set_header Z-Type $zauth_type; proxy_set_header Z-User $zauth_user; + proxy_set_header Z-Client $zauth_client; proxy_set_header Z-Connection $zauth_connection; proxy_set_header Z-Provider $zauth_provider; proxy_set_header Z-Bot $zauth_bot; diff --git a/libs/libzauth/libzauth/rustfmt.toml b/libs/libzauth/libzauth/rustfmt.toml new file mode 100644 index 0000000000..c7ad93bafe --- /dev/null +++ b/libs/libzauth/libzauth/rustfmt.toml @@ -0,0 +1 @@ +disable_all_formatting = true diff --git a/libs/libzauth/libzauth/src/zauth.rs b/libs/libzauth/libzauth/src/zauth.rs index a95fcba2b7..136b6e423b 100644 --- a/libs/libzauth/libzauth/src/zauth.rs +++ b/libs/libzauth/libzauth/src/zauth.rs @@ -214,9 +214,15 @@ mod tests { const ACCESS_TOKEN: &'static str = "aEPOxMwUriGEv2qc7Pb672ygy-6VeJ-8VrX3jmwalZr7xygU4izyCWxiT7IXfybnNGIsk1FQPb0RRVPx1s2UCw==.v=1.k=1.d=1466770783.t=a.l=.u=6562d941-4f40-4db4-b96e-56a06d71c2c3.c=11019722839397809329"; + const ACCESS_TOKEN_CLIENT_ID: &'static str = + "aEPOxMwUriGEv2qc7Pb672ygy-6VeJ-8VrX3jmwalZr7xygU4izyCWxiT7IXfybnNGIsk1FQPb0RRVPx1s2UCw==.v=1.k=1.d=1466770783.t=a.l=.u=6562d941-4f40-4db4-b96e-56a06d71c2c3.c=11019722839397809329.i=deadbeef"; + const USER_TOKEN: &'static str = "vpJs7PEgwtsuzGlMY0-Vqs22s8o9ZDlp7wJrPmhCgIfg0NoTAxvxq5OtknabLMfNTEW9amn5tyeUM7tbFZABBA==.v=1.k=1.d=1466770905.t=u.l=.u=6562d941-4f40-4db4-b96e-56a06d71c2c3.r=4feacc"; + const USER_TOKEN_CLIENT_ID: &'static str = + "vpJs7PEgwtsuzGlMY0-Vqs22s8o9ZDlp7wJrPmhCgIfg0NoTAxvxq5OtknabLMfNTEW9amn5tyeUM7tbFZABBA==.v=1.k=1.d=1466770905.t=u.l=.u=6562d941-4f40-4db4-b96e-56a06d71c2c3.r=4feacc.i=deadbeef"; + const BOT_TOKEN: &'static str = "-cEsTNb68hb-By81MZd5fF6NMDVzR_emkV_HfOnIdZTXsoeRRRZA7hmv9y2uLUNWDifNd-B8u0AjiAT_2rzUDg==.v=1.k=1.d=-1.t=b.l=.p=cd57deb3-bab6-46fd-be28-a3d48ef2c6b7.b=b46833f9-ec2a-4c4a-8304-1a367f849467.c=ae3d1b9e-e47c-4e10-a751-e99a64ada74b"; @@ -245,6 +251,20 @@ mod tests { assert_eq!(t.lookup('c'), Some("11019722839397809329")) } + #[test] + fn parse_access_client_id() { + let t = Token::parse(ACCESS_TOKEN_CLIENT_ID).unwrap(); + assert_eq!(t.signature.to_bytes(), "aEPOxMwUriGEv2qc7Pb672ygy-6VeJ-8VrX3jmwalZr7xygU4izyCWxiT7IXfybnNGIsk1FQPb0RRVPx1s2UCw==".from_base64().unwrap()[..]); + assert_eq!(t.version, 1); + assert_eq!(t.key_idx, 1); + assert_eq!(t.timestamp, 1466770783); + assert_eq!(t.token_tag, None); + assert_eq!(t.token_type, TokenType::Access); + assert_eq!(t.lookup('u'), Some("6562d941-4f40-4db4-b96e-56a06d71c2c3")); + assert_eq!(t.lookup('c'), Some("11019722839397809329")); + assert_eq!(t.lookup('i'), Some("deadbeef")); + } + #[test] fn parse_user() { let t = Token::parse(USER_TOKEN).unwrap(); @@ -258,6 +278,20 @@ mod tests { assert_eq!(t.lookup('r'), Some("4feacc")) } + #[test] + fn parse_user_client_id() { + let t = Token::parse(USER_TOKEN_CLIENT_ID).unwrap(); + assert_eq!(t.signature.to_bytes(), "vpJs7PEgwtsuzGlMY0-Vqs22s8o9ZDlp7wJrPmhCgIfg0NoTAxvxq5OtknabLMfNTEW9amn5tyeUM7tbFZABBA==".from_base64().unwrap()[..]); + assert_eq!(t.version, 1); + assert_eq!(t.key_idx, 1); + assert_eq!(t.timestamp, 1466770905); + assert_eq!(t.token_tag, None); + assert_eq!(t.token_type, TokenType::User); + assert_eq!(t.lookup('u'), Some("6562d941-4f40-4db4-b96e-56a06d71c2c3")); + assert_eq!(t.lookup('r'), Some("4feacc")); + assert_eq!(t.lookup('i'), Some("deadbeef")); + } + #[test] fn parse_bot() { let t = Token::parse(BOT_TOKEN).unwrap(); diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index e3768695ad..c616d1f49c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -21,6 +21,7 @@ module Wire.API.Routes.Public ( -- * nginz combinators ZUser, + ZClient, ZLocalUser, ZConn, ZOptUser, @@ -73,6 +74,7 @@ data ZType ZAuthUser | -- | Same as 'ZAuthUser', but return a 'Local UserId' using the domain in the context ZLocalAuthUser + | ZAuthClient | -- | Get a 'ConnId' from the Z-Conn header ZAuthConn | ZAuthBot @@ -110,6 +112,13 @@ instance IsZType 'ZAuthUser ctx where qualifyZParam _ = id +instance IsZType 'ZAuthClient ctx where + type ZHeader 'ZAuthClient = "Z-Client" + type ZParam 'ZAuthClient = ClientId + type ZQualifiedParam 'ZAuthClient = ClientId + + qualifyZParam _ = id + instance IsZType 'ZAuthConn ctx where type ZHeader 'ZAuthConn = "Z-Connection" type ZParam 'ZAuthConn = ConnId @@ -158,6 +167,8 @@ type ZLocalUser = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts type ZUser = ZAuthServant 'ZAuthUser InternalAuthDefOpts +type ZClient = ZAuthServant 'ZAuthClient InternalAuthDefOpts + type ZConn = ZAuthServant 'ZAuthConn InternalAuthDefOpts type ZBot = ZAuthServant 'ZAuthBot InternalAuthDefOpts diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 3a0d869854..b48d3e1cb7 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1151,9 +1151,10 @@ type AuthAPI = \ Every other combination is invalid.\ \ Access tokens can be given as query parameter or authorisation\ \ header, with the latter being preferred." + :> QueryParam "client_id" ClientId :> Cookies '["zuid" ::: SomeUserToken] - :> CanThrow 'BadCredentials :> Bearer SomeAccessToken + :> CanThrow 'BadCredentials :> MultiVerb1 'POST '[JSON] TokenResponse ) :<|> Named diff --git a/libs/wire-api/src/Wire/API/User/Auth.hs b/libs/wire-api/src/Wire/API/User/Auth.hs index cd217241e6..8a5160daac 100644 --- a/libs/wire-api/src/Wire/API/User/Auth.hs +++ b/libs/wire-api/src/Wire/API/User/Auth.hs @@ -68,7 +68,7 @@ import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as LBS import Data.Code as Code import Data.Handle (Handle) -import Data.Id (UserId) +import Data.Id import Data.Json.Util import Data.Misc (PlainTextPassword (..)) import Data.SOP diff --git a/libs/zauth/default.nix b/libs/zauth/default.nix index 59781bf0b2..9cc4a774c0 100644 --- a/libs/zauth/default.nix +++ b/libs/zauth/default.nix @@ -5,8 +5,8 @@ { mkDerivation, attoparsec, base, base64-bytestring, bytestring , bytestring-conversion, errors, exceptions, gitignoreSource , imports, lens, lib, mtl, mwc-random, optparse-applicative -, sodium-crypto-sign, tasty, tasty-hunit, tasty-quickcheck, time -, uuid, vector +, sodium-crypto-sign, tasty, tasty-hunit, tasty-quickcheck, text +, time, uuid, vector }: mkDerivation { pname = "zauth"; @@ -25,7 +25,7 @@ mkDerivation { ]; testHaskellDepends = [ base bytestring bytestring-conversion errors imports lens - sodium-crypto-sign tasty tasty-hunit tasty-quickcheck uuid + sodium-crypto-sign tasty tasty-hunit tasty-quickcheck text uuid ]; description = "Creation and validation of signed tokens"; license = lib.licenses.agpl3Only; diff --git a/libs/zauth/main/Main.hs b/libs/zauth/main/Main.hs index 5f486acc79..c7f7a187ce 100644 --- a/libs/zauth/main/Main.hs +++ b/libs/zauth/main/Main.hs @@ -82,23 +82,23 @@ go CreateSession o = do let u = uuid . head $ o ^. dat case fromByteString ((o ^. dat) !! 1) of Nothing -> error "invalid random int" - Just rn -> runCreate' o $ toByteString <$> sessionToken (o ^. dur) u rn + Just rn -> runCreate' o $ toByteString <$> sessionToken (o ^. dur) u Nothing rn go CreateUser o = do when (length (o ^. dat) /= 2) $ error "invalid --data, must have 2 elements" let u = uuid . head $ o ^. dat case fromByteString ((o ^. dat) !! 1) of Nothing -> error "invalid random int" - Just rn -> runCreate' o $ toByteString <$> userToken (o ^. dur) u rn + Just rn -> runCreate' o $ toByteString <$> userToken (o ^. dur) u Nothing rn go CreateAccess o = do when (null (o ^. dat)) $ error "invalid --data, must have 1 or 2 elements" let u = uuid . head $ o ^. dat case length (o ^. dat) of - 1 -> runCreate' o $ toByteString <$> accessToken1 (o ^. dur) u + 1 -> runCreate' o $ toByteString <$> accessToken1 (o ^. dur) u Nothing 2 -> case fromByteString ((o ^. dat) !! 1) of Nothing -> error "invalid connection" - Just c -> runCreate' o $ toByteString <$> accessToken (o ^. dur) u c + Just c -> runCreate' o $ toByteString <$> accessToken (o ^. dur) u Nothing c _ -> error "invalid --data, must have 1 or 2 elements" go CreateBot o = do when (length (o ^. dat) /= 3) $ diff --git a/libs/zauth/src/Data/ZAuth/Creation.hs b/libs/zauth/src/Data/ZAuth/Creation.hs index 179317a726..3302428076 100644 --- a/libs/zauth/src/Data/ZAuth/Creation.hs +++ b/libs/zauth/src/Data/ZAuth/Creation.hs @@ -97,46 +97,55 @@ withIndex k m = Create $ do error "withIndex: Key index out of range." local (const (e {keyIdx = k})) (zauth m) -userToken :: Integer -> UUID -> Word32 -> Create (Token User) -userToken dur usr rnd = do +userToken :: Integer -> UUID -> Maybe Text -> Word32 -> Create (Token User) +userToken dur usr cli rnd = do d <- expiry dur - newToken d U Nothing (mkUser usr rnd) + newToken d U Nothing (mkUser usr cli rnd) -sessionToken :: Integer -> UUID -> Word32 -> Create (Token User) -sessionToken dur usr rnd = do +sessionToken :: Integer -> UUID -> Maybe Text -> Word32 -> Create (Token User) +sessionToken dur usr cli rnd = do d <- expiry dur - newToken d U (Just S) (mkUser usr rnd) + newToken d U (Just S) (mkUser usr cli rnd) --- | Create an access token taking a duration, userId and a (random) number that can be used as connection identifier -accessToken :: Integer -> UUID -> Word64 -> Create (Token Access) -accessToken dur usr con = do +-- | Create an access token taking a duration, userId, clientId and a (random) +-- number that can be used as connection identifier +accessToken :: Integer -> UUID -> Maybe Text -> Word64 -> Create (Token Access) +accessToken dur usr cid con = do d <- expiry dur - newToken d A Nothing (mkAccess usr con) + newToken d A Nothing (mkAccess usr cid con) --- | Create an access token taking a duration and userId. Similar to 'accessToken', except that the connection identifier is randomly generated. -accessToken1 :: Integer -> UUID -> Create (Token Access) -accessToken1 dur usr = do +-- | Create an access token taking a duration, userId and clientId. +-- Similar to 'accessToken', except that the connection identifier is randomly +-- generated. +accessToken1 :: Integer -> UUID -> Maybe Text -> Create (Token Access) +accessToken1 dur usr cid = do g <- Create $ asks randGen d <- liftIO $ asGenIO (uniform :: GenIO -> IO Word64) g - accessToken dur usr d + accessToken dur usr cid d -legalHoldUserToken :: Integer -> UUID -> Word32 -> Create (Token LegalHoldUser) -legalHoldUserToken dur usr rnd = do +legalHoldUserToken :: Integer -> UUID -> Maybe Text -> Word32 -> Create (Token LegalHoldUser) +legalHoldUserToken dur usr cli rnd = do d <- expiry dur - newToken d LU Nothing (mkLegalHoldUser usr rnd) - --- | Create a legal hold access token taking a duration, userId and a (random) number that can be used as connection identifier -legalHoldAccessToken :: Integer -> UUID -> Word64 -> Create (Token LegalHoldAccess) -legalHoldAccessToken dur usr con = do + newToken d LU Nothing (mkLegalHoldUser usr cli rnd) + +-- | Create a legal hold access token taking a duration, userId, clientId and a +-- (random) number that can be used as connection identifier +legalHoldAccessToken :: + Integer -> + UUID -> + Maybe Text -> + Word64 -> + Create (Token LegalHoldAccess) +legalHoldAccessToken dur usr cid con = do d <- expiry dur - newToken d LA Nothing (mkLegalHoldAccess usr con) + newToken d LA Nothing (mkLegalHoldAccess usr cid con) -- | Create a legal hold access token taking a duration, userId. Similar to 'legalHoldAccessToken', except that the connection identifier is randomly generated. -legalHoldAccessToken1 :: Integer -> UUID -> Create (Token LegalHoldAccess) -legalHoldAccessToken1 dur usr = do +legalHoldAccessToken1 :: Integer -> UUID -> Maybe Text -> Create (Token LegalHoldAccess) +legalHoldAccessToken1 dur usr cid = do g <- Create $ asks randGen d <- liftIO $ asGenIO (uniform :: GenIO -> IO Word64) g - legalHoldAccessToken dur usr d + legalHoldAccessToken dur usr cid d botToken :: UUID -> UUID -> UUID -> Create (Token Bot) botToken pid bid cnv = newToken (-1) B Nothing (mkBot pid bid cnv) diff --git a/libs/zauth/src/Data/ZAuth/Token.hs b/libs/zauth/src/Data/ZAuth/Token.hs index 375ab26099..83b4ff1f31 100644 --- a/libs/zauth/src/Data/ZAuth/Token.hs +++ b/libs/zauth/src/Data/ZAuth/Token.hs @@ -41,12 +41,14 @@ module Data.ZAuth.Token -- * Access body Access, userId, + clientId, connection, mkAccess, -- * User body User, user, + client, rand, mkUser, @@ -126,6 +128,7 @@ data Header = Header data Access = Access { _userId :: !UUID, + _clientId :: Maybe Text, -- | 'ConnId' is derived from this. _connection :: !Word64 } @@ -133,6 +136,7 @@ data Access = Access data User = User { _user :: !UUID, + _client :: Maybe Text, _rand :: !Word32 } deriving (Eq, Show) @@ -238,10 +242,10 @@ mkToken = Token mkHeader :: Int -> Int -> Integer -> Type -> Maybe Tag -> Header mkHeader = Header -mkAccess :: UUID -> Word64 -> Access +mkAccess :: UUID -> Maybe Text -> Word64 -> Access mkAccess = Access -mkUser :: UUID -> Word32 -> User +mkUser :: UUID -> Maybe Text -> Word32 -> User mkUser = User mkBot :: UUID -> UUID -> UUID -> Bot @@ -250,11 +254,11 @@ mkBot = Bot mkProvider :: UUID -> Provider mkProvider = Provider -mkLegalHoldAccess :: UUID -> Word64 -> LegalHoldAccess -mkLegalHoldAccess uid cid = LegalHoldAccess $ Access uid cid +mkLegalHoldAccess :: UUID -> Maybe Text -> Word64 -> LegalHoldAccess +mkLegalHoldAccess uid clt con = LegalHoldAccess $ Access uid clt con -mkLegalHoldUser :: UUID -> Word32 -> LegalHoldUser -mkLegalHoldUser uid r = LegalHoldUser $ User uid r +mkLegalHoldUser :: UUID -> Maybe Text -> Word32 -> LegalHoldUser +mkLegalHoldUser uid cid r = LegalHoldUser $ User uid cid r ----------------------------------------------------------------------------- -- Reading @@ -295,12 +299,14 @@ readAccessBody :: Properties -> Maybe Access readAccessBody t = Access <$> (lookup "u" t >>= fromLazyASCIIBytes) + <*> pure (lookup "i" t >>= fromByteString') <*> (lookup "c" t >>= fromByteString') readUserBody :: Properties -> Maybe User readUserBody t = User <$> (lookup "u" t >>= fromLazyASCIIBytes) + <*> pure (lookup "i" t >>= fromByteString') <*> (lookup "r" t >>= fmap fromHex . fromByteString') readBotBody :: Properties -> Maybe Bot @@ -346,6 +352,7 @@ writeHeader t = instance ToByteString Access where builder t = field "u" (toLazyASCIIBytes $ t ^. userId) + <> foldMap (\c -> dot <> field "i" c) (t ^. clientId) <> dot <> field "c" (t ^. connection) @@ -354,6 +361,7 @@ instance ToByteString User where field "u" (toLazyASCIIBytes $ t ^. user) <> dot <> field "r" (Hex (t ^. rand)) + <> foldMap (\c -> dot <> field "i" c) (t ^. client) instance ToByteString Bot where builder t = @@ -369,6 +377,7 @@ instance ToByteString Provider where instance ToByteString LegalHoldAccess where builder t = field "u" (toLazyASCIIBytes $ t ^. legalHoldAccess . userId) + <> foldMap (\c -> dot <> field "i" c) (t ^. legalHoldAccess . clientId) <> dot <> field "c" (t ^. legalHoldAccess . connection) @@ -377,6 +386,7 @@ instance ToByteString LegalHoldUser where field "u" (toLazyASCIIBytes $ t ^. legalHoldUser . user) <> dot <> field "r" (Hex (t ^. legalHoldUser . rand)) + <> foldMap (\c -> dot <> field "i" c) (t ^. legalHoldUser . client) instance ToByteString Type where builder A = char8 'a' diff --git a/libs/zauth/test/Arbitraries.hs b/libs/zauth/test/Arbitraries.hs index 16d2f9c8d6..e3284cb218 100644 --- a/libs/zauth/test/Arbitraries.hs +++ b/libs/zauth/test/Arbitraries.hs @@ -22,6 +22,9 @@ module Arbitraries where import Control.Lens ((.~)) +import qualified Data.Text.Lazy as LT +import qualified Data.Text.Lazy.Builder as LT +import qualified Data.Text.Lazy.Builder.Int as LT import Data.UUID hiding (fromString) import Data.ZAuth.Token import Imports @@ -55,11 +58,18 @@ instance Arbitrary Header where <*> arbitrary <*> arbitrary +arbitraryClientId :: Gen (Maybe Text) +arbitraryClientId = + liftArbitrary $ fmap toClientId arbitrary + where + toClientId :: Word64 -> Text + toClientId = LT.toStrict . LT.toLazyText . LT.hexadecimal + instance Arbitrary Access where - arbitrary = mkAccess <$> arbitrary <*> arbitrary + arbitrary = mkAccess <$> arbitrary <*> arbitraryClientId <*> arbitrary instance Arbitrary User where - arbitrary = mkUser <$> arbitrary <*> arbitrary + arbitrary = mkUser <$> arbitrary <*> arbitraryClientId <*> arbitrary instance Arbitrary Bot where arbitrary = mkBot <$> arbitrary <*> arbitrary <*> arbitrary @@ -68,10 +78,10 @@ instance Arbitrary Provider where arbitrary = mkProvider <$> arbitrary instance Arbitrary LegalHoldAccess where - arbitrary = mkLegalHoldAccess <$> arbitrary <*> arbitrary + arbitrary = mkLegalHoldAccess <$> arbitrary <*> arbitraryClientId <*> arbitrary instance Arbitrary LegalHoldUser where - arbitrary = mkLegalHoldUser <$> arbitrary <*> arbitrary + arbitrary = mkLegalHoldUser <$> arbitrary <*> arbitraryClientId <*> arbitrary instance Arbitrary ByteString where arbitrary = fromString <$> arbitrary `suchThat` notElem '.' diff --git a/libs/zauth/test/ZAuth.hs b/libs/zauth/test/ZAuth.hs index 1b6fd4d518..80545f884a 100644 --- a/libs/zauth/test/ZAuth.hs +++ b/libs/zauth/test/ZAuth.hs @@ -89,7 +89,7 @@ testDecEncLegalHoldAccessToken t = fromByteString (toByteString' t) == Just t testNotExpired :: V.Env -> Create () testNotExpired p = do u <- liftIO nextRandom - t <- userToken defDuration u 100 + t <- userToken defDuration u Nothing 100 x <- liftIO $ runValidate p $ check t liftIO $ assertBool "testNotExpired: validation failed" (isRight x) @@ -100,7 +100,7 @@ testNotExpired p = do testExpired :: V.Env -> Create () testExpired p = do u <- liftIO nextRandom - t <- userToken 0 u 100 + t <- userToken 0 u Nothing 100 waitSeconds 1 x <- liftIO $ runValidate p $ check t liftIO $ Left Expired @=? x @@ -110,15 +110,15 @@ testExpired p = do testSignAndVerify :: V.Env -> Create () testSignAndVerify p = do u <- liftIO nextRandom - t <- userToken defDuration u 100 + t <- userToken defDuration u Nothing 100 x <- liftIO $ runValidate p $ check t liftIO $ assertBool "testSignAndVerify: validation failed" (isRight x) testRandDevIds :: Create () testRandDevIds = do u <- liftIO nextRandom - t1 <- view body <$> accessToken1 defDuration u - t2 <- view body <$> accessToken1 defDuration u + t1 <- view body <$> accessToken1 defDuration u Nothing + t2 <- view body <$> accessToken1 defDuration u Nothing liftIO $ assertBool "unexpected: Same device ID." (t1 ^. connection /= t2 ^. connection) -- Helpers: diff --git a/libs/zauth/zauth.cabal b/libs/zauth/zauth.cabal index f38129f894..d81ea68e66 100644 --- a/libs/zauth/zauth.cabal +++ b/libs/zauth/zauth.cabal @@ -212,6 +212,7 @@ test-suite zauth-unit , tasty >=0.9 , tasty-hunit >=0.9 , tasty-quickcheck >=0.8 + , text , uuid , zauth diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index 87c3e166d8..cce5a9e335 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -50,19 +50,25 @@ import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso accessH :: + Maybe ClientId -> [Either Text SomeUserToken] -> Maybe (Either Text SomeAccessToken) -> Handler r SomeAccess -accessH ut' mat' = do +accessH mcid ut' mat' = do ut <- handleTokenErrors ut' mat <- traverse handleTokenError mat' partitionTokens ut mat - >>= either (uncurry access) (uncurry access) + >>= either (uncurry (access mcid)) (uncurry (access mcid)) -access :: TokenPair u a => NonEmpty (Token u) -> Maybe (Token a) -> Handler r SomeAccess -access t mt = +access :: + TokenPair u a => + Maybe ClientId -> + NonEmpty (Token u) -> + Maybe (Token a) -> + Handler r SomeAccess +access mcid t mt = traverse mkUserTokenCookie - =<< wrapHttpClientE (Auth.renewAccess (List1 t) mt) !>> zauthError + =<< wrapHttpClientE (Auth.renewAccess (List1 t) mt mcid) !>> zauthError sendLoginCode :: SendLoginCode -> Handler r LoginCodeTimeout sendLoginCode (SendLoginCode phone call force) = do diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index cb3bb4d3e9..e17535fe5b 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -620,10 +620,10 @@ createUser (Public.NewUserPublic new) = lift . runExceptT $ do Auth.toWebCookie =<< case acc of UserAccount _ Ephemeral -> lift . wrapHttpClient $ - Auth.newCookie @ZAuth.User userId Public.SessionCookie newUserLabel + Auth.newCookie @ZAuth.User userId Nothing Public.SessionCookie newUserLabel UserAccount _ _ -> lift . wrapHttpClient $ - Auth.newCookie @ZAuth.User userId Public.PersistentCookie newUserLabel + Auth.newCookie @ZAuth.User userId Nothing Public.PersistentCookie newUserLabel -- pure $ CreateUserResponse cok userId (Public.SelfProfile usr) pure $ Public.RegisterSuccess cok (Public.SelfProfile usr) where diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 3ec6397f00..f1c104e467 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -44,6 +44,7 @@ import Brig.App import Brig.Budget import qualified Brig.Code as Code import qualified Brig.Data.Activation as Data +import Brig.Data.Client import qualified Brig.Data.LoginCode as Data import qualified Brig.Data.User as Data import Brig.Data.UserKey @@ -63,6 +64,7 @@ import Cassandra import Control.Error hiding (bool) import Control.Lens (to, view) import Control.Monad.Catch +import Control.Monad.Except import Data.ByteString.Conversion (toByteString) import Data.Handle (Handle) import Data.Id @@ -148,7 +150,7 @@ login (PasswordLogin (PasswordLoginData li pw label code)) typ = do AuthEphemeral -> throwE LoginEphemeral AuthPendingInvitation -> throwE LoginPendingActivation verifyLoginCode code uid - wrapHttpClientE $ newAccess @ZAuth.User @ZAuth.Access uid typ label + wrapHttpClientE $ newAccess @ZAuth.User @ZAuth.Access uid Nothing typ label where verifyLoginCode :: Maybe Code.Value -> UserId -> ExceptT LoginError (AppT r) () verifyLoginCode mbCode uid = @@ -165,7 +167,7 @@ login (SmsLogin (SmsLoginData phone code label)) typ = do unless ok $ wrapHttpClientE $ loginFailed uid - wrapHttpClientE $ newAccess @ZAuth.User @ZAuth.Access uid typ label + wrapHttpClientE $ newAccess @ZAuth.User @ZAuth.Access uid Nothing typ label verifyCode :: forall r. @@ -253,12 +255,14 @@ renewAccess :: ) => List1 (ZAuth.Token u) -> Maybe (ZAuth.Token a) -> + Maybe ClientId -> ExceptT ZAuth.Failure m (Access u) -renewAccess uts at = do +renewAccess uts at mcid = do (uid, ck) <- validateTokens uts at + traverse_ (checkClientId uid) mcid lift . Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.renewAccess") catchSuspendInactiveUser uid ZAuth.Expired - ck' <- lift $ nextCookie ck + ck' <- nextCookie ck mcid at' <- lift $ newAccessToken (fromMaybe ck ck') at pure $ Access at' ck' @@ -320,12 +324,13 @@ newAccess :: MonadUnliftIO m ) => UserId -> + Maybe ClientId -> CookieType -> Maybe CookieLabel -> ExceptT LoginError m (Access u) -newAccess uid ct cl = do +newAccess uid cid ct cl = do catchSuspendInactiveUser uid LoginSuspended - r <- lift $ newCookieLimited uid ct cl + r <- lift $ newCookieLimited uid cid ct cl case r of Left delay -> throwE $ LoginThrottled delay Right ck -> do @@ -454,7 +459,7 @@ ssoLogin (SsoLogin uid label) typ = do AuthSuspended -> throwE LoginSuspended AuthEphemeral -> throwE LoginEphemeral AuthPendingInvitation -> throwE LoginPendingActivation - newAccess @ZAuth.User @ZAuth.Access uid typ label + newAccess @ZAuth.User @ZAuth.Access uid Nothing typ label -- | Log in as a LegalHold service, getting LegalHoldUser/Access Tokens. legalHoldLogin :: @@ -472,7 +477,7 @@ legalHoldLogin (LegalHoldLogin uid plainTextPassword label) typ = do Nothing -> throwE LegalHoldLoginNoBindingTeam Just tid -> assertLegalHoldEnabled tid -- create access token and cookie - wrapHttpClientE (newAccess @ZAuth.LegalHoldUser @ZAuth.LegalHoldAccess uid typ label) + wrapHttpClientE (newAccess @ZAuth.LegalHoldUser @ZAuth.LegalHoldAccess uid Nothing typ label) !>> LegalHoldLoginError assertLegalHoldEnabled :: @@ -484,3 +489,7 @@ assertLegalHoldEnabled tid = do case wsStatus stat of FeatureStatusDisabled -> throwE LegalHoldLoginLegalHoldNotEnabled FeatureStatusEnabled -> pure () + +checkClientId :: MonadClient m => UserId -> ClientId -> ExceptT ZAuth.Failure m () +checkClientId uid cid = + lookupClient uid cid >>= maybe (throwE ZAuth.Invalid) (const (pure ())) diff --git a/services/brig/src/Brig/User/Auth/Cookie.hs b/services/brig/src/Brig/User/Auth/Cookie.hs index 0ab37164ea..11b9e6ee60 100644 --- a/services/brig/src/Brig/User/Auth/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/Cookie.hs @@ -46,7 +46,9 @@ import Brig.User.Auth.Cookie.Limit import qualified Brig.User.Auth.DB.Cookie as DB import qualified Brig.ZAuth as ZAuth import Cassandra +import Control.Error import Control.Lens (to, view) +import Control.Monad.Except import Data.ByteString.Conversion import Data.Id import qualified Data.List as List @@ -72,15 +74,16 @@ newCookie :: MonadClient m ) => UserId -> + Maybe ClientId -> CookieType -> Maybe CookieLabel -> m (Cookie (ZAuth.Token u)) -newCookie uid typ label = do +newCookie uid cid typ label = do now <- liftIO =<< view currentTime tok <- if typ == PersistentCookie - then ZAuth.newUserToken uid - else ZAuth.newSessionToken uid + then ZAuth.newUserToken uid cid + else ZAuth.newSessionToken uid cid let c = Cookie { cookieId = CookieId (ZAuth.userTokenRand tok), @@ -104,30 +107,38 @@ nextCookie :: MonadClient m ) => Cookie (ZAuth.Token u) -> - m (Maybe (Cookie (ZAuth.Token u))) -nextCookie c = do + Maybe ClientId -> + ExceptT ZAuth.Failure m (Maybe (Cookie (ZAuth.Token u))) +nextCookie c mNewCid = runMaybeT $ do + let mOldCid = ZAuth.userTokenClient (cookieValue c) + -- If both old and new client IDs are present, they must be equal + when (((/=) <$> mOldCid <*> mNewCid) == Just True) $ + throwError ZAuth.Invalid + -- Keep old client ID by default, but use new one if none was set. + let mcid = mOldCid <|> mNewCid + s <- view settings now <- liftIO =<< view currentTime let created = cookieCreated c let renewAge = fromInteger (setUserCookieRenewAge s) - -- TODO: Also renew the cookie if it was signed with - -- a different zauth key index, regardless of age. - if persist c && diffUTCTime now created > renewAge - then Just <$> getNext - else pure Nothing - where - persist = (PersistentCookie ==) . cookieType - getNext = case cookieSucc c of - Nothing -> renewCookie c - Just ck -> do - let uid = ZAuth.userTokenOf (cookieValue c) - trackSuperseded uid (cookieId c) - cs <- DB.listCookies uid - case List.find (\x -> cookieId x == ck && persist x) cs of - Nothing -> renewCookie c - Just c' -> do - t <- ZAuth.mkUserToken uid (cookieIdNum ck) (cookieExpires c') - pure c' {cookieValue = t} + -- Renew the cookie if the client ID has changed, regardless of age. + -- FUTUREWORK: Also renew the cookie if it was signed with a different zauth + -- key index, regardless of age. + when (mcid == mOldCid) $ do + guard (cookieType c == PersistentCookie) + guard (diffUTCTime now created > renewAge) + lift . lift $ do + c' <- runMaybeT $ do + ck <- hoistMaybe $ cookieSucc c + let uid = ZAuth.userTokenOf (cookieValue c) + lift $ trackSuperseded uid (cookieId c) + cs <- lift $ DB.listCookies uid + c' <- + hoistMaybe $ + List.find (\x -> cookieId x == ck && cookieType x == PersistentCookie) cs + t <- lift $ ZAuth.mkUserToken uid mcid (cookieIdNum ck) (cookieExpires c') + pure c' {cookieValue = t} + maybe (renewCookie c mcid) pure c' -- | Renew the given cookie with a fresh token. renewCookie :: @@ -137,12 +148,13 @@ renewCookie :: MonadClient m ) => Cookie (ZAuth.Token u) -> + Maybe ClientId -> m (Cookie (ZAuth.Token u)) -renewCookie old = do +renewCookie old mcid = do let t = cookieValue old let uid = ZAuth.userTokenOf t -- Insert new cookie - new <- newCookie uid (cookieType old) (cookieLabel old) + new <- newCookie uid mcid (cookieType old) (cookieLabel old) -- Link the old cookie to the new (successor), keeping it -- around only for another renewal period so as not to build -- an ever growing chain of superseded cookies. @@ -230,22 +242,23 @@ newCookieLimited :: ZAuth.MonadZAuth m ) => UserId -> + Maybe ClientId -> CookieType -> Maybe CookieLabel -> m (Either RetryAfter (Cookie (ZAuth.Token t))) -newCookieLimited u typ label = do +newCookieLimited u c typ label = do cs <- filter ((typ ==) . cookieType) <$> DB.listCookies u now <- liftIO =<< view currentTime lim <- CookieLimit . setUserCookieLimit <$> view settings thr <- setUserCookieThrottle <$> view settings let evict = map cookieId (limitCookies lim now cs) if null evict - then Right <$> newCookie u typ label + then Right <$> newCookie u c typ label else case throttleCookies now thr cs of Just wait -> pure (Left wait) Nothing -> do revokeCookies u evict [] - Right <$> newCookie u typ label + Right <$> newCookie u c typ label -------------------------------------------------------------------------------- -- HTTP diff --git a/services/brig/src/Brig/ZAuth.hs b/services/brig/src/Brig/ZAuth.hs index 1a7c2d9403..f0425dd037 100644 --- a/services/brig/src/Brig/ZAuth.hs +++ b/services/brig/src/Brig/ZAuth.hs @@ -69,7 +69,9 @@ module Brig.ZAuth -- * Token Inspection accessTokenOf, + accessTokenClient, userTokenOf, + userTokenClient, legalHoldAccessTokenOf, legalHoldUserTokenOf, userTokenRand, @@ -90,7 +92,8 @@ import Data.Aeson import Data.Bits import qualified Data.ByteString as BS import Data.ByteString.Conversion -import Data.Id +import Data.Id hiding (client) +import qualified Data.Id import Data.List.NonEmpty (NonEmpty, nonEmpty) import qualified Data.List.NonEmpty as NonEmpty import Data.Proxy @@ -234,32 +237,37 @@ instance TokenPair LegalHoldUser LegalHoldAccess where class (FromByteString (Token a), ToByteString a) => AccessTokenLike a where accessTokenOf :: Token a -> UserId + accessTokenClient :: Token a -> Maybe ClientId renewAccessToken :: MonadZAuth m => Token a -> m (Token a) settingsTTL :: Proxy a -> Lens' Settings Integer instance AccessTokenLike Access where accessTokenOf = accessTokenOf' + accessTokenClient = accessTokenClient' renewAccessToken = renewAccessToken' settingsTTL _ = accessTokenTimeout . accessTokenTimeoutSeconds instance AccessTokenLike LegalHoldAccess where accessTokenOf = legalHoldAccessTokenOf + accessTokenClient = legalHoldAccessTokenClient renewAccessToken = renewLegalHoldAccessToken settingsTTL _ = legalHoldAccessTokenTimeout . legalHoldAccessTokenTimeoutSeconds class (FromByteString (Token u), ToByteString u) => UserTokenLike u where userTokenOf :: Token u -> UserId + userTokenClient :: Token u -> Maybe ClientId mkSomeToken :: Token u -> Auth.SomeUserToken - mkUserToken :: MonadZAuth m => UserId -> Word32 -> UTCTime -> m (Token u) + mkUserToken :: MonadZAuth m => UserId -> Maybe ClientId -> Word32 -> UTCTime -> m (Token u) userTokenRand :: Token u -> Word32 - newUserToken :: MonadZAuth m => UserId -> m (Token u) - newSessionToken :: (MonadThrow m, MonadZAuth m) => UserId -> m (Token u) + newUserToken :: MonadZAuth m => UserId -> Maybe ClientId -> m (Token u) + newSessionToken :: (MonadThrow m, MonadZAuth m) => UserId -> Maybe ClientId -> m (Token u) userTTL :: Proxy u -> Lens' Settings Integer zauthType :: Type -- see libs/zauth/src/Token.hs instance UserTokenLike User where mkUserToken = mkUserToken' userTokenOf = userTokenOf' + userTokenClient = userTokenClient' mkSomeToken = Auth.PlainUserToken userTokenRand = userTokenRand' newUserToken = newUserToken' @@ -270,37 +278,38 @@ instance UserTokenLike User where instance UserTokenLike LegalHoldUser where mkUserToken = mkLegalHoldUserToken userTokenOf = legalHoldUserTokenOf + userTokenClient = legalHoldClientTokenOf mkSomeToken = Auth.LHUserToken userTokenRand = legalHoldUserTokenRand newUserToken = newLegalHoldUserToken - newSessionToken _ = throwM ZV.Unsupported + newSessionToken _ _ = throwM ZV.Unsupported userTTL _ = legalHoldUserTokenTimeout . legalHoldUserTokenTimeoutSeconds zauthType = LU -mkUserToken' :: MonadZAuth m => UserId -> Word32 -> UTCTime -> m (Token User) -mkUserToken' u r t = liftZAuth $ do +mkUserToken' :: MonadZAuth m => UserId -> Maybe ClientId -> Word32 -> UTCTime -> m (Token User) +mkUserToken' u cid r t = liftZAuth $ do z <- ask liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ - ZC.newToken (utcTimeToPOSIXSeconds t) U Nothing (mkUser (toUUID u) r) + ZC.newToken (utcTimeToPOSIXSeconds t) U Nothing (mkUser (toUUID u) (fmap Data.Id.client cid) r) -newUserToken' :: MonadZAuth m => UserId -> m (Token User) -newUserToken' u = liftZAuth $ do +newUserToken' :: MonadZAuth m => UserId -> Maybe ClientId -> m (Token User) +newUserToken' u c = liftZAuth $ do z <- ask r <- liftIO randomValue liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ let UserTokenTimeout ttl = z ^. settings . userTokenTimeout - in ZC.userToken ttl (toUUID u) r + in ZC.userToken ttl (toUUID u) (fmap Data.Id.client c) r -newSessionToken' :: MonadZAuth m => UserId -> m (Token User) -newSessionToken' u = liftZAuth $ do +newSessionToken' :: MonadZAuth m => UserId -> Maybe ClientId -> m (Token User) +newSessionToken' u c = liftZAuth $ do z <- ask r <- liftIO randomValue liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ let SessionTokenTimeout ttl = z ^. settings . sessionTokenTimeout - in ZC.sessionToken ttl (toUUID u) r + in ZC.sessionToken ttl (toUUID u) (fmap Data.Id.client c) r newAccessToken' :: MonadZAuth m => Token User -> m (Token Access) newAccessToken' xt = liftZAuth $ do @@ -308,7 +317,7 @@ newAccessToken' xt = liftZAuth $ do liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ let AccessTokenTimeout ttl = z ^. settings . accessTokenTimeout - in ZC.accessToken1 ttl (xt ^. body . user) + in ZC.accessToken1 ttl (xt ^. body . user) (xt ^. body . client) renewAccessToken' :: MonadZAuth m => Token Access -> m (Token Access) renewAccessToken' old = liftZAuth $ do @@ -339,21 +348,31 @@ newProviderToken pid = liftZAuth $ do -- 2) (mkLegalHoldUser uid r) / (mkUser uid r) -- Possibly some duplication could be removed. -- See https://github.com/wireapp/wire-server/pull/761/files#r318612423 -mkLegalHoldUserToken :: MonadZAuth m => UserId -> Word32 -> UTCTime -> m (Token LegalHoldUser) -mkLegalHoldUserToken u r t = liftZAuth $ do +mkLegalHoldUserToken :: + MonadZAuth m => + UserId -> + Maybe ClientId -> + Word32 -> + UTCTime -> + m (Token LegalHoldUser) +mkLegalHoldUserToken u c r t = liftZAuth $ do z <- ask liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ - ZC.newToken (utcTimeToPOSIXSeconds t) LU Nothing (mkLegalHoldUser (toUUID u) r) - -newLegalHoldUserToken :: MonadZAuth m => UserId -> m (Token LegalHoldUser) -newLegalHoldUserToken u = liftZAuth $ do + ZC.newToken + (utcTimeToPOSIXSeconds t) + LU + Nothing + (mkLegalHoldUser (toUUID u) (fmap Data.Id.client c) r) + +newLegalHoldUserToken :: MonadZAuth m => UserId -> Maybe ClientId -> m (Token LegalHoldUser) +newLegalHoldUserToken u c = liftZAuth $ do z <- ask r <- liftIO randomValue liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ let LegalHoldUserTokenTimeout ttl = z ^. settings . legalHoldUserTokenTimeout - in ZC.legalHoldUserToken ttl (toUUID u) r + in ZC.legalHoldUserToken ttl (toUUID u) (fmap Data.Id.client c) r newLegalHoldAccessToken :: MonadZAuth m => Token LegalHoldUser -> m (Token LegalHoldAccess) newLegalHoldAccessToken xt = liftZAuth $ do @@ -361,7 +380,10 @@ newLegalHoldAccessToken xt = liftZAuth $ do liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ let LegalHoldAccessTokenTimeout ttl = z ^. settings . legalHoldAccessTokenTimeout - in ZC.legalHoldAccessToken1 ttl (xt ^. body . legalHoldUser . user) + in ZC.legalHoldAccessToken1 + ttl + (xt ^. body . legalHoldUser . user) + (xt ^. body . legalHoldUser . client) renewLegalHoldAccessToken :: MonadZAuth m => Token LegalHoldAccess -> m (Token LegalHoldAccess) renewLegalHoldAccessToken old = liftZAuth $ do @@ -382,15 +404,27 @@ validateToken t = liftZAuth $ do accessTokenOf' :: Token Access -> UserId accessTokenOf' t = Id (t ^. body . userId) +accessTokenClient' :: Token Access -> Maybe ClientId +accessTokenClient' t = fmap ClientId (t ^. body . clientId) + userTokenOf' :: Token User -> UserId userTokenOf' t = Id (t ^. body . user) +userTokenClient' :: Token User -> Maybe ClientId +userTokenClient' t = fmap ClientId (t ^. body . client) + legalHoldAccessTokenOf :: Token LegalHoldAccess -> UserId legalHoldAccessTokenOf t = Id (t ^. body . legalHoldAccess . userId) +legalHoldAccessTokenClient :: Token LegalHoldAccess -> Maybe ClientId +legalHoldAccessTokenClient t = fmap ClientId (t ^. body . legalHoldAccess . clientId) + legalHoldUserTokenOf :: Token LegalHoldUser -> UserId legalHoldUserTokenOf t = Id (t ^. body . legalHoldUser . user) +legalHoldClientTokenOf :: Token LegalHoldUser -> Maybe ClientId +legalHoldClientTokenOf t = fmap ClientId (t ^. body . legalHoldUser . client) + userTokenRand' :: Token User -> Word32 userTokenRand' t = t ^. body . rand diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 5ef3548900..01cb9db293 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -37,7 +37,7 @@ import qualified Brig.ZAuth as ZAuth import qualified Cassandra as DB import Control.Lens (set, (^.)) import Control.Retry -import Data.Aeson as Aeson +import Data.Aeson as Aeson hiding (json) import qualified Data.ByteString as BS import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as Lazy @@ -71,6 +71,7 @@ import qualified Wire.API.User.Auth as Auth import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso +import Wire.API.User.Client -- | FUTUREWORK: Implement this function. This wrapper should make sure that -- wrapped tests run only when the feature flag 'legalhold' is set to @@ -148,7 +149,10 @@ tests conf m z db b g n = test m "token mismatch" (onlyIfLhWhitelisted (testTokenMismatchLegalhold z b g)), test m "new-persistent-cookie" (testNewPersistentCookie conf b), test m "new-session-cookie" (testNewSessionCookie conf b), - test m "suspend-inactive" (testSuspendInactiveUsers conf b) + test m "suspend-inactive" (testSuspendInactiveUsers conf b), + test m "client access" (testAccessWithClientId b), + test m "client access incorrect" (testAccessWithIncorrectClientId b), + test m "multiple client accesses" (testAccessWithExistingClientId b) ], testGroup "update /access/self/email" @@ -178,7 +182,9 @@ randomAccessToken :: forall u a. ZAuth.TokenPair u a => ZAuth (ZAuth.Token a) randomAccessToken = randomUserToken @u >>= ZAuth.newAccessToken randomUserToken :: ZAuth.UserTokenLike u => ZAuth (ZAuth.Token u) -randomUserToken = liftIO UUID.nextRandom >>= ZAuth.newUserToken . Id +randomUserToken = do + r <- Id <$> liftIO UUID.nextRandom + ZAuth.newUserToken r Nothing ------------------------------------------------------------------------------- -- Nginz authentication tests (end-to-end sanity checks) @@ -773,7 +779,7 @@ testInvalidCookie z b = do -- Expired user <- userId <$> randomUser b let f = set (ZAuth.userTTL (Proxy @u)) 0 - t <- toByteString' <$> runZAuth z (ZAuth.localSettings f (ZAuth.newUserToken @u user)) + t <- toByteString' <$> runZAuth z (ZAuth.localSettings f (ZAuth.newUserToken @u user Nothing)) liftIO $ threadDelay 1000000 post (unversioned . b . path "/access" . cookieRaw "zuid" t) !!! do const 403 === statusCode @@ -784,7 +790,7 @@ testInvalidCookie z b = do testInvalidToken :: ZAuth.Env -> Brig -> Http () testInvalidToken z b = do user <- userId <$> randomUser b - t <- toByteString' <$> runZAuth z (ZAuth.newUserToken @ZAuth.User user) + t <- toByteString' <$> runZAuth z (ZAuth.newUserToken @ZAuth.User user Nothing) -- Syntactically invalid post (unversioned . b . path "/access" . queryItem "access_token" "xxx" . cookieRaw "zuid" t) @@ -968,6 +974,148 @@ getAndTestDBSupersededCookieAndItsValidSuccessor config b n = do -- and a valid cookie pure (c, c') +testAccessWithClientId :: Brig -> Http () +testAccessWithClientId brig = do + u <- randomUser brig + rs <- + login + brig + ( emailLogin + (fromJust (userEmail u)) + defPassword + (Just "nexus1") + ) + PersistentCookie + Http () +testAccessWithIncorrectClientId brig = do + u <- randomUser brig + rs <- + login + brig + ( emailLogin + (fromJust (userEmail u)) + defPassword + (Just "nexus1") + ) + PersistentCookie + Http () +testAccessWithExistingClientId brig = do + u <- randomUser brig + rs <- + login + brig + ( emailLogin + (fromJust (userEmail u)) + defPassword + (Just "nexus1") + ) + PersistentCookie + Brig -> Http () testNewSessionCookie config b = do u <- randomUser b diff --git a/services/nginz/third_party/nginx-zauth-module/README.md b/services/nginz/third_party/nginx-zauth-module/README.md index 5b89793500..1fc539420e 100644 --- a/services/nginz/third_party/nginx-zauth-module/README.md +++ b/services/nginz/third_party/nginx-zauth-module/README.md @@ -31,15 +31,17 @@ the vector of keys in declaration order. zauth_key "secret3"; } -### $zauth_user, $zauth_connection +### $zauth_user, $zauth_client, $zauth_connection -These variables are replaced with the actual user/connection IDs of an -access-token and set in upstream requests as headers "Z-User"/"Z-Connection". +These variables are replaced with the actual user/client/connection IDs of an +access-token and set in upstream requests as headers "Z-User" / "Z-Client" / +"Z-Connection". *Example*: location /upstream { proxy_set_header "Z-User" $zauth_user; + proxy_set_header "Z-Client" $zauth_client; proxy_set_header "Z-Connection" $zauth_connection; proxy_pass http://localhost:9000; } diff --git a/services/nginz/third_party/nginx-zauth-module/zauth_module.c b/services/nginz/third_party/nginx-zauth-module/zauth_module.c index ca7e878ad3..c9d2ce3540 100644 --- a/services/nginz/third_party/nginx-zauth-module/zauth_module.c +++ b/services/nginz/third_party/nginx-zauth-module/zauth_module.c @@ -371,6 +371,7 @@ static ngx_int_t zauth_variables (ngx_conf_t * conf) { ngx_str_t z_prov_id = ngx_string("zauth_provider"); ngx_str_t z_bot_id = ngx_string("zauth_bot"); ngx_str_t z_user_id = ngx_string("zauth_user"); + ngx_str_t z_client_id = ngx_string("zauth_client"); ngx_str_t z_conn_id = ngx_string("zauth_connection"); ngx_str_t z_conv_id = ngx_string("zauth_conversation"); @@ -386,6 +387,9 @@ static ngx_int_t zauth_variables (ngx_conf_t * conf) { ngx_http_variable_t * z_user_var = ngx_http_add_variable(conf, &z_user_id, NGX_HTTP_VAR_NOHASH); + ngx_http_variable_t * z_client_var = + ngx_http_add_variable(conf, &z_client_id, NGX_HTTP_VAR_NOHASH); + ngx_http_variable_t * z_conn_var = ngx_http_add_variable(conf, &z_conn_id, NGX_HTTP_VAR_NOHASH); @@ -393,7 +397,8 @@ static ngx_int_t zauth_variables (ngx_conf_t * conf) { ngx_http_add_variable(conf, &z_conv_id, NGX_HTTP_VAR_NOHASH); if ( z_type_var == NULL || z_prov_var == NULL || z_bot_var == NULL || - z_user_var == NULL || z_conn_var == NULL || z_conv_var == NULL ) + z_user_var == NULL || z_client_var == NULL || z_conn_var == NULL || + z_conv_var == NULL ) { return NGX_ERROR; } @@ -403,6 +408,8 @@ static ngx_int_t zauth_variables (ngx_conf_t * conf) { z_bot_var->data = 'b'; z_user_var->get_handler = zauth_token_var; z_user_var->data = 'u'; + z_client_var->get_handler = zauth_token_var; + z_client_var->data = 'i'; z_prov_var->get_handler = zauth_token_var; z_prov_var->data = 'p'; z_conn_var->get_handler = zauth_token_var_conn;