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

Commit 637df95

Browse files
authored
Support configuring the lifetime of non-refreshable access tokens separately to refreshable access tokens. (#11445)
1 parent e5f426c commit 637df95

File tree

5 files changed

+221
-3
lines changed

5 files changed

+221
-3
lines changed

changelog.d/11445.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support configuring the lifetime of non-refreshable access tokens separately to refreshable access tokens.

synapse/config/registration.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,60 @@ def read_config(self, config, **kwargs):
130130
int
131131
] = refreshable_access_token_lifetime
132132

133+
if (
134+
self.session_lifetime is not None
135+
and "refreshable_access_token_lifetime" in config
136+
):
137+
if self.session_lifetime < self.refreshable_access_token_lifetime:
138+
raise ConfigError(
139+
"Both `session_lifetime` and `refreshable_access_token_lifetime` "
140+
"configuration options have been set, but `refreshable_access_token_lifetime` "
141+
" exceeds `session_lifetime`!"
142+
)
143+
144+
# The `nonrefreshable_access_token_lifetime` applies for tokens that can NOT be
145+
# refreshed using a refresh token.
146+
# If it is None, then these tokens last for the entire length of the session,
147+
# which is infinite by default.
148+
# The intention behind this configuration option is to help with requiring
149+
# all clients to use refresh tokens, if the homeserver administrator requires.
150+
nonrefreshable_access_token_lifetime = config.get(
151+
"nonrefreshable_access_token_lifetime",
152+
None,
153+
)
154+
if nonrefreshable_access_token_lifetime is not None:
155+
nonrefreshable_access_token_lifetime = self.parse_duration(
156+
nonrefreshable_access_token_lifetime
157+
)
158+
self.nonrefreshable_access_token_lifetime = nonrefreshable_access_token_lifetime
159+
160+
if (
161+
self.session_lifetime is not None
162+
and self.nonrefreshable_access_token_lifetime is not None
163+
):
164+
if self.session_lifetime < self.nonrefreshable_access_token_lifetime:
165+
raise ConfigError(
166+
"Both `session_lifetime` and `nonrefreshable_access_token_lifetime` "
167+
"configuration options have been set, but `nonrefreshable_access_token_lifetime` "
168+
" exceeds `session_lifetime`!"
169+
)
170+
133171
refresh_token_lifetime = config.get("refresh_token_lifetime")
134172
if refresh_token_lifetime is not None:
135173
refresh_token_lifetime = self.parse_duration(refresh_token_lifetime)
136174
self.refresh_token_lifetime: Optional[int] = refresh_token_lifetime
137175

176+
if (
177+
self.session_lifetime is not None
178+
and self.refresh_token_lifetime is not None
179+
):
180+
if self.session_lifetime < self.refresh_token_lifetime:
181+
raise ConfigError(
182+
"Both `session_lifetime` and `refresh_token_lifetime` "
183+
"configuration options have been set, but `refresh_token_lifetime` "
184+
" exceeds `session_lifetime`!"
185+
)
186+
138187
# The fallback template used for authenticating using a registration token
139188
self.registration_token_template = self.read_template("registration_token.html")
140189

synapse/handlers/register.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Copyright 2014 - 2016 OpenMarket Ltd
2+
# Copyright 2021 The Matrix.org Foundation C.I.C.
23
#
34
# Licensed under the Apache License, Version 2.0 (the "License");
45
# you may not use this file except in compliance with the License.
@@ -116,6 +117,9 @@ def __init__(self, hs: "HomeServer"):
116117
self.pusher_pool = hs.get_pusherpool()
117118

118119
self.session_lifetime = hs.config.registration.session_lifetime
120+
self.nonrefreshable_access_token_lifetime = (
121+
hs.config.registration.nonrefreshable_access_token_lifetime
122+
)
119123
self.refreshable_access_token_lifetime = (
120124
hs.config.registration.refreshable_access_token_lifetime
121125
)
@@ -794,13 +798,25 @@ async def register_device_inner(
794798
class and RegisterDeviceReplicationServlet.
795799
"""
796800
assert not self.hs.config.worker.worker_app
801+
now_ms = self.clock.time_msec()
797802
access_token_expiry = None
798803
if self.session_lifetime is not None:
799804
if is_guest:
800805
raise Exception(
801806
"session_lifetime is not currently implemented for guest access"
802807
)
803-
access_token_expiry = self.clock.time_msec() + self.session_lifetime
808+
access_token_expiry = now_ms + self.session_lifetime
809+
810+
if self.nonrefreshable_access_token_lifetime is not None:
811+
if access_token_expiry is not None:
812+
# Don't allow the non-refreshable access token to outlive the
813+
# session.
814+
access_token_expiry = min(
815+
now_ms + self.nonrefreshable_access_token_lifetime,
816+
access_token_expiry,
817+
)
818+
else:
819+
access_token_expiry = now_ms + self.nonrefreshable_access_token_lifetime
804820

805821
refresh_token = None
806822
refresh_token_id = None
@@ -818,8 +834,6 @@ class and RegisterDeviceReplicationServlet.
818834
# that this value is set before setting this flag).
819835
assert self.refreshable_access_token_lifetime is not None
820836

821-
now_ms = self.clock.time_msec()
822-
823837
# Set the expiry time of the refreshable access token
824838
access_token_expiry = now_ms + self.refreshable_access_token_lifetime
825839

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright 2021 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+
from synapse.config import ConfigError
15+
from synapse.config.homeserver import HomeServerConfig
16+
17+
from tests.unittest import TestCase
18+
from tests.utils import default_config
19+
20+
21+
class RegistrationConfigTestCase(TestCase):
22+
def test_session_lifetime_must_not_be_exceeded_by_smaller_lifetimes(self):
23+
"""
24+
session_lifetime should logically be larger than, or at least as large as,
25+
all the different token lifetimes.
26+
Test that the user is faced with configuration errors if they make it
27+
smaller, as that configuration doesn't make sense.
28+
"""
29+
config_dict = default_config("test")
30+
31+
# First test all the error conditions
32+
with self.assertRaises(ConfigError):
33+
HomeServerConfig().parse_config_dict(
34+
{
35+
"session_lifetime": "30m",
36+
"nonrefreshable_access_token_lifetime": "31m",
37+
**config_dict,
38+
}
39+
)
40+
41+
with self.assertRaises(ConfigError):
42+
HomeServerConfig().parse_config_dict(
43+
{
44+
"session_lifetime": "30m",
45+
"refreshable_access_token_lifetime": "31m",
46+
**config_dict,
47+
}
48+
)
49+
50+
with self.assertRaises(ConfigError):
51+
HomeServerConfig().parse_config_dict(
52+
{
53+
"session_lifetime": "30m",
54+
"refresh_token_lifetime": "31m",
55+
**config_dict,
56+
}
57+
)
58+
59+
# Then test all the fine conditions
60+
HomeServerConfig().parse_config_dict(
61+
{
62+
"session_lifetime": "31m",
63+
"nonrefreshable_access_token_lifetime": "31m",
64+
**config_dict,
65+
}
66+
)
67+
68+
HomeServerConfig().parse_config_dict(
69+
{
70+
"session_lifetime": "31m",
71+
"refreshable_access_token_lifetime": "31m",
72+
**config_dict,
73+
}
74+
)
75+
76+
HomeServerConfig().parse_config_dict(
77+
{"session_lifetime": "31m", "refresh_token_lifetime": "31m", **config_dict}
78+
)

tests/rest/client/test_auth.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,19 @@ def use_refresh_token(self, refresh_token: str) -> FakeChannel:
524524
{"refresh_token": refresh_token},
525525
)
526526

527+
def is_access_token_valid(self, access_token) -> bool:
528+
"""
529+
Checks whether an access token is valid, returning whether it is or not.
530+
"""
531+
code = self.make_request(
532+
"GET", "/_matrix/client/v3/account/whoami", access_token=access_token
533+
).code
534+
535+
# Either 200 or 401 is what we get back; anything else is a bug.
536+
assert code in {HTTPStatus.OK, HTTPStatus.UNAUTHORIZED}
537+
538+
return code == HTTPStatus.OK
539+
527540
def test_login_issue_refresh_token(self):
528541
"""
529542
A login response should include a refresh_token only if asked.
@@ -671,6 +684,69 @@ def test_refreshable_access_token_expiration(self):
671684
HTTPStatus.UNAUTHORIZED,
672685
)
673686

687+
@override_config(
688+
{
689+
"refreshable_access_token_lifetime": "1m",
690+
"nonrefreshable_access_token_lifetime": "10m",
691+
}
692+
)
693+
def test_different_expiry_for_refreshable_and_nonrefreshable_access_tokens(self):
694+
"""
695+
Tests that the expiry times for refreshable and non-refreshable access
696+
tokens can be different.
697+
"""
698+
body = {
699+
"type": "m.login.password",
700+
"user": "test",
701+
"password": self.user_pass,
702+
}
703+
login_response1 = self.make_request(
704+
"POST",
705+
"/_matrix/client/r0/login",
706+
{"org.matrix.msc2918.refresh_token": True, **body},
707+
)
708+
self.assertEqual(login_response1.code, 200, login_response1.result)
709+
self.assertApproximates(
710+
login_response1.json_body["expires_in_ms"], 60 * 1000, 100
711+
)
712+
refreshable_access_token = login_response1.json_body["access_token"]
713+
714+
login_response2 = self.make_request(
715+
"POST",
716+
"/_matrix/client/r0/login",
717+
body,
718+
)
719+
self.assertEqual(login_response2.code, 200, login_response2.result)
720+
nonrefreshable_access_token = login_response2.json_body["access_token"]
721+
722+
# Advance 59 seconds in the future (just shy of 1 minute, the time of expiry)
723+
self.reactor.advance(59.0)
724+
725+
# Both tokens should still be valid.
726+
self.assertTrue(self.is_access_token_valid(refreshable_access_token))
727+
self.assertTrue(self.is_access_token_valid(nonrefreshable_access_token))
728+
729+
# Advance to 61 s (just past 1 minute, the time of expiry)
730+
self.reactor.advance(2.0)
731+
732+
# Only the non-refreshable token is still valid.
733+
self.assertFalse(self.is_access_token_valid(refreshable_access_token))
734+
self.assertTrue(self.is_access_token_valid(nonrefreshable_access_token))
735+
736+
# Advance to 599 s (just shy of 10 minutes, the time of expiry)
737+
self.reactor.advance(599.0 - 61.0)
738+
739+
# It's still the case that only the non-refreshable token is still valid.
740+
self.assertFalse(self.is_access_token_valid(refreshable_access_token))
741+
self.assertTrue(self.is_access_token_valid(nonrefreshable_access_token))
742+
743+
# Advance to 601 s (just past 10 minutes, the time of expiry)
744+
self.reactor.advance(2.0)
745+
746+
# Now neither token is valid.
747+
self.assertFalse(self.is_access_token_valid(refreshable_access_token))
748+
self.assertFalse(self.is_access_token_valid(nonrefreshable_access_token))
749+
674750
@override_config(
675751
{"refreshable_access_token_lifetime": "1m", "refresh_token_lifetime": "2m"}
676752
)

0 commit comments

Comments
 (0)