Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 5d4c330

Browse files
authored
Allow re-using a UI auth validation for a period of time (#8970)
1 parent 4136255 commit 5d4c330

File tree

10 files changed

+193
-49
lines changed

10 files changed

+193
-49
lines changed

changelog.d/8970.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow re-using an user-interactive authentication session for a period of time.

docs/sample_config.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2068,6 +2068,21 @@ password_config:
20682068
#
20692069
#require_uppercase: true
20702070

2071+
ui_auth:
2072+
# The number of milliseconds to allow a user-interactive authentication
2073+
# session to be active.
2074+
#
2075+
# This defaults to 0, meaning the user is queried for their credentials
2076+
# before every action, but this can be overridden to alow a single
2077+
# validation to be re-used. This weakens the protections afforded by
2078+
# the user-interactive authentication process, by allowing for multiple
2079+
# (and potentially different) operations to use the same validation session.
2080+
#
2081+
# Uncomment below to allow for credential validation to last for 15
2082+
# seconds.
2083+
#
2084+
#session_timeout: 15000
2085+
20712086

20722087
# Configuration for sending emails from Synapse.
20732088
#

synapse/config/_base.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ from typing import Any, Iterable, List, Optional
33
from synapse.config import (
44
api,
55
appservice,
6+
auth,
67
captcha,
78
cas,
89
consent_config,
@@ -14,7 +15,6 @@ from synapse.config import (
1415
logger,
1516
metrics,
1617
oidc_config,
17-
password,
1818
password_auth_providers,
1919
push,
2020
ratelimiting,
@@ -65,7 +65,7 @@ class RootConfig:
6565
sso: sso.SSOConfig
6666
oidc: oidc_config.OIDCConfig
6767
jwt: jwt_config.JWTConfig
68-
password: password.PasswordConfig
68+
auth: auth.AuthConfig
6969
email: emailconfig.EmailConfig
7070
worker: workers.WorkerConfig
7171
authproviders: password_auth_providers.PasswordAuthProviderConfig

synapse/config/password.py renamed to synapse/config/auth.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
# Copyright 2015, 2016 OpenMarket Ltd
3+
# Copyright 2020 The Matrix.org Foundation C.I.C.
34
#
45
# Licensed under the Apache License, Version 2.0 (the "License");
56
# you may not use this file except in compliance with the License.
@@ -16,11 +17,11 @@
1617
from ._base import Config
1718

1819

19-
class PasswordConfig(Config):
20-
"""Password login configuration
20+
class AuthConfig(Config):
21+
"""Password and login configuration
2122
"""
2223

23-
section = "password"
24+
section = "auth"
2425

2526
def read_config(self, config, **kwargs):
2627
password_config = config.get("password_config", {})
@@ -35,6 +36,10 @@ def read_config(self, config, **kwargs):
3536
self.password_policy = password_config.get("policy") or {}
3637
self.password_policy_enabled = self.password_policy.get("enabled", False)
3738

39+
# User-interactive authentication
40+
ui_auth = config.get("ui_auth") or {}
41+
self.ui_auth_session_timeout = ui_auth.get("session_timeout", 0)
42+
3843
def generate_config_section(self, config_dir_path, server_name, **kwargs):
3944
return """\
4045
password_config:
@@ -87,4 +92,19 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs):
8792
# Defaults to 'false'.
8893
#
8994
#require_uppercase: true
95+
96+
ui_auth:
97+
# The number of milliseconds to allow a user-interactive authentication
98+
# session to be active.
99+
#
100+
# This defaults to 0, meaning the user is queried for their credentials
101+
# before every action, but this can be overridden to alow a single
102+
# validation to be re-used. This weakens the protections afforded by
103+
# the user-interactive authentication process, by allowing for multiple
104+
# (and potentially different) operations to use the same validation session.
105+
#
106+
# Uncomment below to allow for credential validation to last for 15
107+
# seconds.
108+
#
109+
#session_timeout: 15000
90110
"""

synapse/config/homeserver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ._base import RootConfig
1818
from .api import ApiConfig
1919
from .appservice import AppServiceConfig
20+
from .auth import AuthConfig
2021
from .cache import CacheConfig
2122
from .captcha import CaptchaConfig
2223
from .cas import CasConfig
@@ -30,7 +31,6 @@
3031
from .logger import LoggingConfig
3132
from .metrics import MetricsConfig
3233
from .oidc_config import OIDCConfig
33-
from .password import PasswordConfig
3434
from .password_auth_providers import PasswordAuthProviderConfig
3535
from .push import PushConfig
3636
from .ratelimiting import RatelimitConfig
@@ -76,7 +76,7 @@ class HomeServerConfig(RootConfig):
7676
CasConfig,
7777
SSOConfig,
7878
JWTConfig,
79-
PasswordConfig,
79+
AuthConfig,
8080
EmailConfig,
8181
PasswordAuthProviderConfig,
8282
PushConfig,

synapse/handlers/auth.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@ def __init__(self, hs: "HomeServer"):
226226
burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
227227
)
228228

229+
# The number of seconds to keep a UI auth session active.
230+
self._ui_auth_session_timeout = hs.config.ui_auth_session_timeout
231+
229232
# Ratelimitier for failed /login attempts
230233
self._failed_login_attempts_ratelimiter = Ratelimiter(
231234
clock=hs.get_clock(),
@@ -283,7 +286,7 @@ async def validate_user_via_ui_auth(
283286
request_body: Dict[str, Any],
284287
clientip: str,
285288
description: str,
286-
) -> Tuple[dict, str]:
289+
) -> Tuple[dict, Optional[str]]:
287290
"""
288291
Checks that the user is who they claim to be, via a UI auth.
289292
@@ -310,7 +313,8 @@ async def validate_user_via_ui_auth(
310313
have been given only in a previous call).
311314
312315
'session_id' is the ID of this session, either passed in by the
313-
client or assigned by this call
316+
client or assigned by this call. This is None if UI auth was
317+
skipped (by re-using a previous validation).
314318
315319
Raises:
316320
InteractiveAuthIncompleteError if the client has not yet completed
@@ -324,6 +328,16 @@ async def validate_user_via_ui_auth(
324328
325329
"""
326330

331+
if self._ui_auth_session_timeout:
332+
last_validated = await self.store.get_access_token_last_validated(
333+
requester.access_token_id
334+
)
335+
if self.clock.time_msec() - last_validated < self._ui_auth_session_timeout:
336+
# Return the input parameters, minus the auth key, which matches
337+
# the logic in check_ui_auth.
338+
request_body.pop("auth", None)
339+
return request_body, None
340+
327341
user_id = requester.user.to_string()
328342

329343
# Check if we should be ratelimited due to too many previous failed attempts
@@ -359,6 +373,9 @@ async def validate_user_via_ui_auth(
359373
if user_id != requester.user.to_string():
360374
raise AuthError(403, "Invalid auth")
361375

376+
# Note that the access token has been validated.
377+
await self.store.update_access_token_last_validated(requester.access_token_id)
378+
362379
return params, session_id
363380

364381
async def _get_available_ui_auth_types(self, user: UserID) -> Iterable[str]:
@@ -452,13 +469,10 @@ async def check_ui_auth(
452469
all the stages in any of the permitted flows.
453470
"""
454471

455-
authdict = None
456472
sid = None # type: Optional[str]
457-
if clientdict and "auth" in clientdict:
458-
authdict = clientdict["auth"]
459-
del clientdict["auth"]
460-
if "session" in authdict:
461-
sid = authdict["session"]
473+
authdict = clientdict.pop("auth", {})
474+
if "session" in authdict:
475+
sid = authdict["session"]
462476

463477
# Convert the URI and method to strings.
464478
uri = request.uri.decode("utf-8")
@@ -563,6 +577,8 @@ async def check_ui_auth(
563577

564578
creds = await self.store.get_completed_ui_auth_stages(session.session_id)
565579
for f in flows:
580+
# If all the required credentials have been supplied, the user has
581+
# successfully completed the UI auth process!
566582
if len(set(f) - set(creds)) == 0:
567583
# it's very useful to know what args are stored, but this can
568584
# include the password in the case of registering, so only log

synapse/rest/client/v2_alpha/account.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,14 +254,18 @@ async def on_POST(self, request):
254254
logger.error("Auth succeeded but no known type! %r", result.keys())
255255
raise SynapseError(500, "", Codes.UNKNOWN)
256256

257-
# If we have a password in this request, prefer it. Otherwise, there
258-
# must be a password hash from an earlier request.
257+
# If we have a password in this request, prefer it. Otherwise, use the
258+
# password hash from an earlier request.
259259
if new_password:
260260
password_hash = await self.auth_handler.hash(new_password)
261-
else:
261+
elif session_id is not None:
262262
password_hash = await self.auth_handler.get_session_data(
263263
session_id, "password_hash", None
264264
)
265+
else:
266+
# UI validation was skipped, but the request did not include a new
267+
# password.
268+
password_hash = None
265269
if not password_hash:
266270
raise SynapseError(400, "Missing params: password", Codes.MISSING_PARAM)
267271

synapse/storage/databases/main/registration.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,42 @@ async def del_user_pending_deactivation(self, user_id: str) -> None:
943943
desc="del_user_pending_deactivation",
944944
)
945945

946+
async def get_access_token_last_validated(self, token_id: int) -> int:
947+
"""Retrieves the time (in milliseconds) of the last validation of an access token.
948+
949+
Args:
950+
token_id: The ID of the access token to update.
951+
Raises:
952+
StoreError if the access token was not found.
953+
954+
Returns:
955+
The last validation time.
956+
"""
957+
result = await self.db_pool.simple_select_one_onecol(
958+
"access_tokens", {"id": token_id}, "last_validated"
959+
)
960+
961+
# If this token has not been validated (since starting to track this),
962+
# return 0 instead of None.
963+
return result or 0
964+
965+
async def update_access_token_last_validated(self, token_id: int) -> None:
966+
"""Updates the last time an access token was validated.
967+
968+
Args:
969+
token_id: The ID of the access token to update.
970+
Raises:
971+
StoreError if there was a problem updating this.
972+
"""
973+
now = self._clock.time_msec()
974+
975+
await self.db_pool.simple_update_one(
976+
"access_tokens",
977+
{"id": token_id},
978+
{"last_validated": now},
979+
desc="update_access_token_last_validated",
980+
)
981+
946982

947983
class RegistrationBackgroundUpdateStore(RegistrationWorkerStore):
948984
def __init__(self, database: DatabasePool, db_conn: Connection, hs: "HomeServer"):
@@ -1150,6 +1186,7 @@ async def add_access_token_to_user(
11501186
The token ID
11511187
"""
11521188
next_id = self._access_tokens_id_gen.get_next()
1189+
now = self._clock.time_msec()
11531190

11541191
await self.db_pool.simple_insert(
11551192
"access_tokens",
@@ -1160,6 +1197,7 @@ async def add_access_token_to_user(
11601197
"device_id": device_id,
11611198
"valid_until_ms": valid_until_ms,
11621199
"puppets_user_id": puppets_user_id,
1200+
"last_validated": now,
11631201
},
11641202
desc="add_access_token_to_user",
11651203
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/* Copyright 2020 The Matrix.org Foundation C.I.C
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
-- The last time this access token was "validated" (i.e. logged in or succeeded
17+
-- at user-interactive authentication).
18+
ALTER TABLE access_tokens ADD COLUMN last_validated BIGINT;

0 commit comments

Comments
 (0)