Skip to content

Commit ad2c0b9

Browse files
committed
Allow unsuspended users to login even if inactive.
There is a way to auto-suspend inactive users after a configurable time span. This change makes sure that expired cookies that would trigger auto-suspend immediately are removed on re-activation.
1 parent 9467cc6 commit ad2c0b9

12 files changed

Lines changed: 91 additions & 29 deletions

File tree

libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ data AuthenticationSubsystem m a where
7878
SameLabelPolicy ->
7979
AuthenticationSubsystem m (Either RetryAfter (Cookie (ZAuth.Token t)))
8080
RevokeCookies :: UserId -> [CookieId] -> [CookieLabel] -> AuthenticationSubsystem m ()
81+
RevokeAllExpiredCookies :: UserId -> AuthenticationSubsystem m ()
8182
-- Verification Codes
8283
EnforceVerificationCodeEither :: Local UserId -> Maybe Code.Value -> VerificationAction -> AuthenticationSubsystem m (Either VerificationCodeError ())
8384
-- For testing

libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Config.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import Data.Vector qualified as Vector
2626
import Data.ZAuth.Creation qualified as ZC
2727
import Imports
2828
import Sodium.Crypto.Sign
29+
import Util.Timeout
2930
import Wire.API.Allowlists (AllowlistEmailDomains)
3031
import Wire.AuthenticationSubsystem.Cookie.Limit
3132

@@ -35,7 +36,8 @@ data AuthenticationSubsystemConfig = AuthenticationSubsystemConfig
3536
zauthEnv :: ZAuthEnv,
3637
userCookieRenewAge :: Integer,
3738
userCookieLimit :: Int,
38-
userCookieThrottle :: CookieThrottle
39+
userCookieThrottle :: CookieThrottle,
40+
suspendInactiveUsers :: Maybe Timeout
3941
}
4042

4143
data ZAuthSettings = ZAuthSettings

libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Cookie.hs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ module Wire.AuthenticationSubsystem.Cookie where
1919

2020
import Data.Id
2121
import Data.RetryAfter
22+
import Data.Time
2223
import Data.ZAuth.CryptoSign (CryptoSign)
2324
import Data.ZAuth.Token
2425
import Imports
2526
import Polysemy
2627
import Polysemy.Error
2728
import Polysemy.Input
29+
import Util.Timeout
2830
import Wire.API.User.Auth
2931
import Wire.API.UserEvent (UserEvent (UserSessionRefreshSuggested))
3032
import Wire.AuthenticationSubsystem
@@ -142,3 +144,30 @@ revokeCookiesMatchingExcept u mself ids labels = do
142144
&& ( c.cookieId `elem` ids
143145
|| maybe False (`elem` labels) c.cookieLabel
144146
)
147+
148+
-- Remove stale cookies. Stale means either (1) cookie is expired, or
149+
-- (2) cookie creation time is further in the past than
150+
-- `env.suspendInactiveUsers` allows.
151+
revokeAllExpiredCookiesImpl ::
152+
( Member SessionStore r,
153+
Member (Input AuthenticationSubsystemConfig) r,
154+
Member Now r
155+
) =>
156+
UserId ->
157+
Sem r ()
158+
revokeAllExpiredCookiesImpl uid = do
159+
now :: UTCTime <- Now.get
160+
mbSuspendAge <- (.suspendInactiveUsers) <$> input
161+
162+
let dead :: Cookie () -> Bool
163+
dead c = cookieExpired && userInactive
164+
where
165+
cookieExpired = c.cookieExpires < now
166+
userInactive =
167+
maybe
168+
False
169+
(\suspendAge -> c.cookieCreated < addUTCTime (-(timeoutDiff suspendAge)) now)
170+
mbSuspendAge
171+
172+
cc <- filter dead <$> SessionStore.listCookies uid
173+
SessionStore.deleteCookies uid cc

libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ interpretAuthenticationSubsystem userSubsystemInterpreter =
107107
NewCookie uid mcid typ mLabel policy -> newCookieImpl uid mcid typ mLabel policy
108108
NewCookieLimited uid mcid typ mLabel policy -> runError $ newCookieLimitedImpl uid mcid typ mLabel policy
109109
RevokeCookies uid ids labels -> revokeCookiesImpl uid ids labels
110+
RevokeAllExpiredCookies uid -> revokeAllExpiredCookiesImpl uid
110111
-- Verification Codes
111112
EnforceVerificationCodeEither luid mCode action -> runError $ enforceVerificationCodeImpl luid mCode action
112113
-- Testing

libs/wire-subsystems/test/unit/Wire/MiniBackend.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,8 @@ defaultAuthenticationSubsystemConfig =
461461
local = defaultLocalDomain,
462462
userCookieRenewAge = 2,
463463
userCookieLimit = 5,
464-
userCookieThrottle = StdDevThrottle 5 3
464+
userCookieThrottle = StdDevThrottle 5 3,
465+
suspendInactiveUsers = Nothing
465466
}
466467

467468
defaultLocalDomain :: Local ()

services/brig/src/Brig/API/Internal.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ teamsAPI ::
315315
Member (Polysemy.Error UserSubsystemError) r,
316316
Member Events r,
317317
Member (Input (Local ())) r,
318+
Member AuthenticationSubsystem r,
318319
Member IndexedUserStore r
319320
) =>
320321
ServerT BrigIRoutes.TeamsAPI (Handler r)
@@ -782,7 +783,8 @@ getPasswordResetCode email =
782783
changeAccountStatusH ::
783784
( Member UserSubsystem r,
784785
Member Events r,
785-
Member UserStore r
786+
Member UserStore r,
787+
Member AuthenticationSubsystem r
786788
) =>
787789
UserId ->
788790
AccountStatusUpdate ->

services/brig/src/Brig/API/User.hs

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ import Wire.API.UserEvent
120120
import Wire.ActivationCodeStore
121121
import Wire.ActivationCodeStore qualified as ActivationCode
122122
import Wire.AuthenticationSubsystem (AuthenticationSubsystem, internalLookupPasswordResetCode)
123+
import Wire.AuthenticationSubsystem qualified as Auth
123124
import Wire.BackendNotificationQueueAccess
124125
import Wire.BlockListStore as BlockListStore
125126
import Wire.ClientStore (ClientStore)
@@ -626,45 +627,60 @@ changeAccountStatus ::
626627
( Member (Concurrency 'Unsafe) r,
627628
Member UserSubsystem r,
628629
Member Events r,
629-
Member UserStore r
630+
Member UserStore r,
631+
Member AuthenticationSubsystem r
630632
) =>
631633
NonEmpty UserId ->
632634
AccountStatus ->
633635
ExceptT AccountStatusError (AppT r) ()
634636
changeAccountStatus usrs status = do
635-
-- It is safe to not revoke any cookies here; if no valid access
636-
-- token is available, cookies are only validated when calling `POST
637-
-- /access`, and access token refresh only works on unsuspended
638-
-- users.
639-
--
640-
-- Evidence: `git grep -Hn --color=never 'UserToken\b' | grep libs/wire-api/src/Wire/API/Routes/Public/`.
641637
ev <- mkUserEvent status
642-
lift $ liftSem $ unsafePooledMapConcurrentlyN_ 16 (update ev) usrs
643-
where
644-
update ::
645-
(UserId -> UserEvent) ->
646-
UserId ->
647-
Sem r ()
648-
update ev u = do
649-
UserStore.updateAccountStatus u status
650-
User.internalUpdateSearchIndex u
651-
Events.generateUserEvent u Nothing (ev u)
638+
lift $ liftSem $ unsafePooledMapConcurrentlyN_ 16 (changeSingleAccountStatusInternal status ev) usrs
652639

653640
changeSingleAccountStatus ::
654641
( Member UserSubsystem r,
655642
Member Events r,
656-
Member UserStore r
643+
Member UserStore r,
644+
Member AuthenticationSubsystem r
657645
) =>
658646
UserId ->
659647
AccountStatus ->
660648
ExceptT AccountStatusError (AppT r) ()
661649
changeSingleAccountStatus uid status = do
662650
unlessM (lift . liftSem $ UserStore.doesUserExist uid) $ throwE AccountNotFound
663651
ev <- mkUserEvent status
664-
lift . liftSem $ do
665-
UserStore.updateAccountStatus uid status
666-
User.internalUpdateSearchIndex uid
667-
Events.generateUserEvent uid Nothing (ev uid)
652+
lift . liftSem $ changeSingleAccountStatusInternal status ev uid
653+
654+
changeSingleAccountStatusInternal ::
655+
( Member UserSubsystem r,
656+
Member Events r,
657+
Member UserStore r,
658+
Member AuthenticationSubsystem r
659+
) =>
660+
AccountStatus ->
661+
(UserId -> UserEvent) ->
662+
UserId ->
663+
Sem r ()
664+
changeSingleAccountStatusInternal status ev u = do
665+
-- It is safe to *not* revoke any cookies here; if no valid access
666+
-- token is available, cookies are only validated when calling `POST
667+
-- /access`, and access token refresh only works on unsuspended
668+
-- users.
669+
--
670+
-- Evidence: `git grep -Hn --color=never 'UserToken\b' | grep libs/wire-api/src/Wire/API/Routes/Public/`.
671+
--
672+
-- Having that said, we need to remove all *expired* cookies here,
673+
-- otherwise /login considers the user inactive, see
674+
-- 'mustSuspendInactiveUser'.
675+
--
676+
-- The intuition is that every change of account status can be
677+
-- considered an account activity, so users that have their status
678+
-- changed recently should not be considered inactive, even if they
679+
-- haven't taken any action themselves.
680+
Auth.revokeAllExpiredCookies u
681+
UserStore.updateAccountStatus u status
682+
User.internalUpdateSearchIndex u
683+
Events.generateUserEvent u Nothing (ev u)
668684

669685
mkUserEvent ::
670686
(Monad m) =>

services/brig/src/Brig/CanonicalInterpreter.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import Brig.Effects.SFT (SFT, interpretSFT)
2828
import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore)
2929
import Brig.Effects.UserPendingActivationStore.Cassandra (userPendingActivationStoreToCassandra)
3030
import Brig.IO.Intra (runEvents)
31-
import Brig.Options (Settings (consumableNotifications), federationDomainConfigs, federationStrategy)
31+
import Brig.Options (Settings (consumableNotifications), SuspendInactiveUsers (..), federationDomainConfigs, federationStrategy)
3232
import Brig.Options qualified as Opt
3333
import Brig.Template (InvitationUrlTemplates)
3434
import Brig.User.Search.Index (IndexEnv (..))
@@ -352,7 +352,8 @@ runBrigToIO e (AppT ma) = do
352352
local = localUnit,
353353
userCookieRenewAge = e.settings.userCookieRenewAge,
354354
userCookieLimit = e.settings.userCookieLimit,
355-
userCookieThrottle = e.settings.userCookieThrottle
355+
userCookieThrottle = e.settings.userCookieThrottle,
356+
suspendInactiveUsers = suspendTimeout <$> e.settings.suspendInactiveUsers
356357
}
357358
mainESEnv = e.indexEnv ^. to idxElastic
358359
indexedUserStoreConfig =

services/brig/src/Brig/Team/API.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import Wire.API.Team.Member qualified as Teams
6969
import Wire.API.Team.Permission (Perm (AddTeamMember))
7070
import Wire.API.Team.Size
7171
import Wire.API.User hiding (fromEmail)
72+
import Wire.AuthenticationSubsystem qualified as Auth
7273
import Wire.BlockListStore
7374
import Wire.EmailSubsystem.Interpreter (renderInvitationUrl)
7475
import Wire.Error
@@ -372,6 +373,7 @@ suspendTeam ::
372373
Member Events r,
373374
Member TinyLog r,
374375
Member InvitationStore r,
376+
Member Auth.AuthenticationSubsystem r,
375377
Member UserStore r
376378
) =>
377379
TeamId ->
@@ -392,6 +394,7 @@ unsuspendTeam ::
392394
Member UserSubsystem r,
393395
Member TeamSubsystem r,
394396
Member Events r,
397+
Member Auth.AuthenticationSubsystem r,
395398
Member UserStore r
396399
) =>
397400
TeamId ->
@@ -410,6 +413,7 @@ changeTeamAccountStatuses ::
410413
Member TeamSubsystem r,
411414
Member UserSubsystem r,
412415
Member Events r,
416+
Member Auth.AuthenticationSubsystem r,
413417
Member UserStore r
414418
) =>
415419
TeamId ->

services/brig/src/Brig/User/Auth.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,8 @@ catchSuspendInactiveUser ::
235235
( Member TinyLog r,
236236
Member UserSubsystem r,
237237
Member Events r,
238-
Member UserStore r
238+
Member UserStore r,
239+
Member AuthenticationSubsystem r
239240
) =>
240241
UserId ->
241242
e ->

0 commit comments

Comments
 (0)