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

Commit 1035663

Browse files
authored
Add config for customizing the claim used for JWT logins. (#11361)
Allows specifying a different claim (from the default "sub") to use when calculating the localpart of the Matrix ID used during the JWT login.
1 parent 3d893b8 commit 1035663

File tree

6 files changed

+57
-35
lines changed

6 files changed

+57
-35
lines changed

changelog.d/11361.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update the JWT login type to support custom a `sub` claim.

docs/jwt.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ will be removed in a future version of Synapse.
2222

2323
The `token` field should include the JSON web token with the following claims:
2424

25-
* The `sub` (subject) claim is required and should encode the local part of the
26-
user ID.
25+
* A claim that encodes the local part of the user ID is required. By default,
26+
the `sub` (subject) claim is used, or a custom claim can be set in the
27+
configuration file.
2728
* The expiration time (`exp`), not before time (`nbf`), and issued at (`iat`)
2829
claims are optional, but validated if present.
2930
* The issuer (`iss`) claim is optional, but required and validated if configured.

docs/sample_config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2039,6 +2039,12 @@ sso:
20392039
#
20402040
#algorithm: "provided-by-your-issuer"
20412041

2042+
# Name of the claim containing a unique identifier for the user.
2043+
#
2044+
# Optional, defaults to `sub`.
2045+
#
2046+
#subject_claim: "sub"
2047+
20422048
# The issuer to validate the "iss" claim against.
20432049
#
20442050
# Optional, if provided the "iss" claim will be required and

synapse/config/jwt.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def read_config(self, config, **kwargs):
3131
self.jwt_secret = jwt_config["secret"]
3232
self.jwt_algorithm = jwt_config["algorithm"]
3333

34+
self.jwt_subject_claim = jwt_config.get("subject_claim", "sub")
35+
3436
# The issuer and audiences are optional, if provided, it is asserted
3537
# that the claims exist on the JWT.
3638
self.jwt_issuer = jwt_config.get("issuer")
@@ -46,6 +48,7 @@ def read_config(self, config, **kwargs):
4648
self.jwt_enabled = False
4749
self.jwt_secret = None
4850
self.jwt_algorithm = None
51+
self.jwt_subject_claim = None
4952
self.jwt_issuer = None
5053
self.jwt_audiences = None
5154

@@ -88,6 +91,12 @@ def generate_config_section(self, **kwargs):
8891
#
8992
#algorithm: "provided-by-your-issuer"
9093
94+
# Name of the claim containing a unique identifier for the user.
95+
#
96+
# Optional, defaults to `sub`.
97+
#
98+
#subject_claim: "sub"
99+
91100
# The issuer to validate the "iss" claim against.
92101
#
93102
# Optional, if provided the "iss" claim will be required and

synapse/rest/client/login.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def __init__(self, hs: "HomeServer"):
7272
# JWT configuration variables.
7373
self.jwt_enabled = hs.config.jwt.jwt_enabled
7474
self.jwt_secret = hs.config.jwt.jwt_secret
75+
self.jwt_subject_claim = hs.config.jwt.jwt_subject_claim
7576
self.jwt_algorithm = hs.config.jwt.jwt_algorithm
7677
self.jwt_issuer = hs.config.jwt.jwt_issuer
7778
self.jwt_audiences = hs.config.jwt.jwt_audiences
@@ -413,7 +414,7 @@ async def _do_jwt_login(
413414
errcode=Codes.FORBIDDEN,
414415
)
415416

416-
user = payload.get("sub", None)
417+
user = payload.get(self.jwt_subject_claim, None)
417418
if user is None:
418419
raise LoginError(403, "Invalid JWT", errcode=Codes.FORBIDDEN)
419420

tests/rest/client/test_login.py

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -815,13 +815,20 @@ class JWTTestCase(unittest.HomeserverTestCase):
815815

816816
jwt_secret = "secret"
817817
jwt_algorithm = "HS256"
818+
base_config = {
819+
"enabled": True,
820+
"secret": jwt_secret,
821+
"algorithm": jwt_algorithm,
822+
}
818823

819-
def make_homeserver(self, reactor, clock):
820-
self.hs = self.setup_test_homeserver()
821-
self.hs.config.jwt.jwt_enabled = True
822-
self.hs.config.jwt.jwt_secret = self.jwt_secret
823-
self.hs.config.jwt.jwt_algorithm = self.jwt_algorithm
824-
return self.hs
824+
def default_config(self):
825+
config = super().default_config()
826+
827+
# If jwt_config has been defined (eg via @override_config), don't replace it.
828+
if config.get("jwt_config") is None:
829+
config["jwt_config"] = self.base_config
830+
831+
return config
825832

826833
def jwt_encode(self, payload: Dict[str, Any], secret: str = jwt_secret) -> str:
827834
# PyJWT 2.0.0 changed the return type of jwt.encode from bytes to str.
@@ -879,16 +886,7 @@ def test_login_no_sub(self):
879886
self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN")
880887
self.assertEqual(channel.json_body["error"], "Invalid JWT")
881888

882-
@override_config(
883-
{
884-
"jwt_config": {
885-
"jwt_enabled": True,
886-
"secret": jwt_secret,
887-
"algorithm": jwt_algorithm,
888-
"issuer": "test-issuer",
889-
}
890-
}
891-
)
889+
@override_config({"jwt_config": {**base_config, "issuer": "test-issuer"}})
892890
def test_login_iss(self):
893891
"""Test validating the issuer claim."""
894892
# A valid issuer.
@@ -919,16 +917,7 @@ def test_login_iss_no_config(self):
919917
self.assertEqual(channel.result["code"], b"200", channel.result)
920918
self.assertEqual(channel.json_body["user_id"], "@kermit:test")
921919

922-
@override_config(
923-
{
924-
"jwt_config": {
925-
"jwt_enabled": True,
926-
"secret": jwt_secret,
927-
"algorithm": jwt_algorithm,
928-
"audiences": ["test-audience"],
929-
}
930-
}
931-
)
920+
@override_config({"jwt_config": {**base_config, "audiences": ["test-audience"]}})
932921
def test_login_aud(self):
933922
"""Test validating the audience claim."""
934923
# A valid audience.
@@ -962,6 +951,19 @@ def test_login_aud_no_config(self):
962951
channel.json_body["error"], "JWT validation failed: Invalid audience"
963952
)
964953

954+
def test_login_default_sub(self):
955+
"""Test reading user ID from the default subject claim."""
956+
channel = self.jwt_login({"sub": "kermit"})
957+
self.assertEqual(channel.result["code"], b"200", channel.result)
958+
self.assertEqual(channel.json_body["user_id"], "@kermit:test")
959+
960+
@override_config({"jwt_config": {**base_config, "subject_claim": "username"}})
961+
def test_login_custom_sub(self):
962+
"""Test reading user ID from a custom subject claim."""
963+
channel = self.jwt_login({"username": "frog"})
964+
self.assertEqual(channel.result["code"], b"200", channel.result)
965+
self.assertEqual(channel.json_body["user_id"], "@frog:test")
966+
965967
def test_login_no_token(self):
966968
params = {"type": "org.matrix.login.jwt"}
967969
channel = self.make_request(b"POST", LOGIN_URL, params)
@@ -1024,12 +1026,14 @@ class JWTPubKeyTestCase(unittest.HomeserverTestCase):
10241026
]
10251027
)
10261028

1027-
def make_homeserver(self, reactor, clock):
1028-
self.hs = self.setup_test_homeserver()
1029-
self.hs.config.jwt.jwt_enabled = True
1030-
self.hs.config.jwt.jwt_secret = self.jwt_pubkey
1031-
self.hs.config.jwt.jwt_algorithm = "RS256"
1032-
return self.hs
1029+
def default_config(self):
1030+
config = super().default_config()
1031+
config["jwt_config"] = {
1032+
"enabled": True,
1033+
"secret": self.jwt_pubkey,
1034+
"algorithm": "RS256",
1035+
}
1036+
return config
10331037

10341038
def jwt_encode(self, payload: Dict[str, Any], secret: str = jwt_privatekey) -> str:
10351039
# PyJWT 2.0.0 changed the return type of jwt.encode from bytes to str.

0 commit comments

Comments
 (0)