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

Customize claim for local part of JWT logins #11361

Merged
merged 12 commits into from
Nov 22, 2021
1 change: 1 addition & 0 deletions changelog.d/9493.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update JWT login type to support custom sub claim.
5 changes: 3 additions & 2 deletions docs/jwt.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ will be removed in a future version of Synapse.

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

* The `sub` (subject) claim is required and should encode the local part of the
user ID.
* A claim that encodes the local part of the user ID is required. By default,
the `sub` (subject) claim is used, or a custom claim can be set in the
configuration file.
* The expiration time (`exp`), not before time (`nbf`), and issued at (`iat`)
claims are optional, but validated if present.
* The issuer (`iss`) claim is optional, but required and validated if configured.
Expand Down
6 changes: 6 additions & 0 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2030,6 +2030,12 @@ sso:
#
#secret: "provided-by-your-issuer"

# Name of the claim containing a unique identifier for the user.
#
# Optional, defaults to `sub`.
#
#subject_claim: "sub"

# The algorithm used to sign the JSON web token.
#
# Supported algorithms are listed at
Expand Down
8 changes: 8 additions & 0 deletions synapse/config/jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def read_config(self, config, **kwargs):

# The issuer and audiences are optional, if provided, it is asserted
# that the claims exist on the JWT.
self.jwt_subject_claim = jwt_config.get("subject_claim", "sub")
vrinek marked this conversation as resolved.
Show resolved Hide resolved
self.jwt_issuer = jwt_config.get("issuer")
self.jwt_audiences = jwt_config.get("audiences")

Expand All @@ -48,6 +49,7 @@ def read_config(self, config, **kwargs):
self.jwt_algorithm = None
self.jwt_issuer = None
self.jwt_audiences = None
self.jwt_subject_claim = None
vrinek marked this conversation as resolved.
Show resolved Hide resolved

def generate_config_section(self, **kwargs):
return """\
Expand Down Expand Up @@ -79,6 +81,12 @@ def generate_config_section(self, **kwargs):
#
#secret: "provided-by-your-issuer"

# Name of the claim containing a unique identifier for the user.
#
# Optional, defaults to `sub`.
#
#subject_claim: "sub"
vrinek marked this conversation as resolved.
Show resolved Hide resolved

# The algorithm used to sign the JSON web token.
#
# Supported algorithms are listed at
Expand Down
4 changes: 3 additions & 1 deletion synapse/rest/client/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def __init__(self, hs: "HomeServer"):
# JWT configuration variables.
self.jwt_enabled = hs.config.jwt.jwt_enabled
self.jwt_secret = hs.config.jwt.jwt_secret
self.jwt_subject_claim = hs.config.jwt.jwt_subject_claim
self.jwt_algorithm = hs.config.jwt.jwt_algorithm
self.jwt_issuer = hs.config.jwt.jwt_issuer
self.jwt_audiences = hs.config.jwt.jwt_audiences
Expand Down Expand Up @@ -413,7 +414,8 @@ async def _do_jwt_login(
errcode=Codes.FORBIDDEN,
)

user = payload.get("sub", None)
subject_claim = self.jwt_subject_claim or "sub"
user = payload.get(subject_claim, None)
clokep marked this conversation as resolved.
Show resolved Hide resolved
if user is None:
raise LoginError(403, "Invalid JWT", errcode=Codes.FORBIDDEN)

Expand Down
22 changes: 22 additions & 0 deletions tests/rest/client/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,28 @@ def test_login_aud_no_config(self):
channel.json_body["error"], "JWT validation failed: Invalid audience"
)

def test_login_default_sub(self):
"""Test reading user ID from the default subject claim."""
channel = self.jwt_login({"sub": "kermit"})
self.assertEqual(channel.result["code"], b"200", channel.result)
self.assertEqual(channel.json_body["user_id"], "@kermit:test")

@override_config(
{
"jwt_config": {
"jwt_enabled": True,
"secret": jwt_secret,
"algorithm": jwt_algorithm,
"subject_claim": "username",
}
}
)
def test_login_custom_sub(self):
"""Test reading user ID from a custom subject claim."""
channel = self.jwt_login({"username": "frog"})
self.assertEqual(channel.result["code"], b"200", channel.result)
self.assertEqual(channel.json_body["user_id"], "@frog:test")

def test_login_no_token(self):
params = {"type": "org.matrix.login.jwt"}
channel = self.make_request(b"POST", LOGIN_URL, params)
Expand Down