-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Update JWT login type to support JWKS, custom sub claim, and encode special chars in user ID #9493
Conversation
Signed-off-by: Botond Horvath <[email protected]>
… ID for JWT logins Signed-off-by: Botond Horvath <[email protected]>
…logins Signed-off-by: Botond Horvath <[email protected]>
Signed-off-by: Botond Horvath <[email protected]>
Looks like reasonable improvements! 👍 This seems broken on Python 3.5 (which pyjwt technically doesn't support: jpadilla/pyjwt#585, Synapse will be dropping it shortly). Also could you see if any of the documentation under |
@boti7 It seems that some unrelated changes were pushed onto this branch for CI. If those are improvements that would be useful we should do them as a separate PR. Did you see my comment above? Note that we'll be dropping support for Python 3.5 soon, so might make sense to just wait on that if possible. |
@clokep this was my fault. We'll roll it all back. |
(Also merging in develop should fix the mypy issues.) |
14e8bca
to
d8920fd
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty reasonable overall. I suspect it would be a nice follow-up to move the JWT code into a separate handler instead of sprinkling it into LoginRestServlet
.
|
||
if not key and self.jwt_jwks_uri: | ||
jwks_client = PyJWKClient(self.jwt_jwks_uri) | ||
signing_key = jwks_client.get_signing_key_from_jwt(token) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this make a synchronous HTTP call? If so we ideally would do this via a SimpleHttpClient
or push this into the background.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, seems like it makes a synchronous HTTP call. It is also possible to implement the loading and parsing of JWKS without PyJWT, like in OidcHandler
, then we have more control over the request and caching.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fetching this for every login seems quite inefficient. I wonder if we should do something on start-up (like OIDC). It looks like PyJWKClient
is quite simple!
I just made a small change to From my side it's totally fine to wait until you drop support for Python 3.5. Also merged in develop, but there are still some mypy issues. |
We've now dropped Python 3.5 support 🎉 |
I think we're still waiting for some changes here? If nothing else there is a merge conflict. @boti7 please nudge if/when its ready for review 🙂 |
@boti7 I'll clean up this conflict and PR. Desperately need escaped sub claim. Auth0 lovingly uses a pipe to separate auth method and ID. |
Rebase JWT-auth changes onto upstream/develop
Looks like the JWT tests are failing when using old-deps, possibly need to bump the minimum version of the library? |
@erikjohnston It's ready for review from my side. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good overall! I wonder if there's enough JWT logic here now that we should move this code to a separate JwtHandler
. What do you think?
@@ -107,7 +107,7 @@ | |||
"url_preview": ["lxml>=3.5.0"], | |||
"sentry": ["sentry-sdk>=0.7.2"], | |||
"opentracing": ["jaeger-client>=4.0.0", "opentracing>=2.2.0"], | |||
"jwt": ["pyjwt>=1.6.4"], | |||
"jwt": ["pyjwt>=2.1.0"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I asked the packages whether this change would be OK. Can you remind me why we're bumping this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like supported versions of Debian and Fedora are on 1.7.1 still. Would it be possible to use that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PyJWKClient
was added in v2.0.0 and caching of signing keys is supported since v2.1.0: https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst#v210
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not really sure what to do about this, although I do wonder if we should be using PyJWKClient
since it bypasses the reactor / current methods for communicating with external resources.
|
||
if not key and self.jwt_jwks_uri: | ||
jwks_client = PyJWKClient(self.jwt_jwks_uri) | ||
signing_key = jwks_client.get_signing_key_from_jwt(token) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fetching this for every login seems quite inefficient. I wonder if we should do something on start-up (like OIDC). It looks like PyJWKClient
is quite simple!
subject_claim = self.jwt_subject_claim or "sub" | ||
user = payload.get(subject_claim, None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already did the fallback to sub
in the config code, no need to do it again.
subject_claim = self.jwt_subject_claim or "sub" | |
user = payload.get(subject_claim, None) | |
user = payload.get(self.jwt_subject_claim, None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue is that tests don't use the config code but set hs.config
directly, so without specifying a fallback here, many tests in JWTTestCase
would fail. What do you suggest to solve this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The config code should get run during tests, see:
Lines 469 to 472 in fe604a0
# Parse the config from a config dict into a HomeServerConfig | |
config_obj = HomeServerConfig() | |
config_obj.parse_config_dict(config, "", "") | |
kwargs["config"] = config_obj |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Were you able to try this again? It should run fine during tests.
Co-authored-by: Patrick Cloke <[email protected]>
Removing my review -- this has a couple of open questions still that are waiting for responses! |
Hey @boti7, are you able to continue working on this? 🙂 Also note that there is now a conflict in |
Given the lack of activity, I'm going to close this. @boti7 feel free to reopen if you'd like to continue working on this! Anyone else is free to pick it back up in a new PR as well 🙂 |
This PR adds the following optional fields to the JWT login type configuration:
jwks_uri
allows specifying a JSON Web Key Set endpoint where RSA signing keys can be retrieved from, instead of using a secret or public key from the configuration.subject_claim
allows specifying a claim other than the default "sub" to use as localpart of the Matrix ID.normalize_user_id
enables mapping the localpart from other character sets using themap_username_to_mxid_localpart
function.Pull Request Checklist
EventStore
toEventWorkerStore
.".code blocks
.Signed-off-by: Botond Horvath [email protected]