-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Conversation
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.
this looks really great - thank you! A few stylistic bits and pieces below.
I'm not really familiar with OAuth2, so I'm largely taking it on trust that this does what it is supposed to do. Are there any well-known services (google? github?) that this will work with?
A document in the docs
folder giving a few notes on how to configure OAuth2 integration might be helpful?
# limitations under the License. | ||
import logging | ||
|
||
from six.moves import urllib |
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 don't need to support python 2 any more; please use urllib
directly rather than using six
.
return userinfo | ||
|
||
def get_server_redirect_url(self): | ||
return self.public_baseurl + b"_matrix/client/r0/login/oauth/response" |
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.
can we not store this in a field in the constructor, rather than having to call a method?
) | ||
return result | ||
|
||
async def get_access_token(self, oauth2_code): |
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.
for internal methods like this, please could you prefix them with _
to show that they are internal.
(alternatively, if they form part of the public API of the class, they need docstrings.)
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.
(tbh it could do with a docstring anyway: in this case it should clarify that this method is fetching the OAuth2 access token rather than a matrix access token.)
self.oauth2_server_userinfo_url = hs.config.oauth2_server_userinfo_url | ||
self.oauth2_client_id = hs.config.oauth2_client_id | ||
self.oauth2_client_secret = hs.config.oauth2_client_secret | ||
self.oauth2_scope = "openid" |
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.
why are these object-level fields rather than file-level constants?
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 response_type and response_mode should be hardcoded as the code flow is the only one implemented yet. The scope on the other hand should be configurable.
self.oauth2_response_type = "code" | ||
self.oauth2_response_mode = "query" | ||
|
||
# state |
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.
please can you document the structure of this. what does it map from and to?
@@ -0,0 +1,57 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright 2015, 2016 OpenMarket Ltd |
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.
/me checks calendar.
Also please don't assign copyright for new code to OpenMarket unless you work for them? You can either put your own name here or omit the copyright line altogether.
@@ -0,0 +1,154 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright 2019 The Matrix.org Foundation C.I.C. |
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.
likewise, it's not really sensible to put Matrix.org here unless you work for them.
@@ -600,5 +624,8 @@ def register_servlets(hs, http_server): | |||
if hs.config.cas_enabled: | |||
CasRedirectServlet(hs).register(http_server) | |||
CasTicketServlet(hs).register(http_server) | |||
elif hs.config.oauth2_enabled: |
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 probably need to add a check somewhere that will make synapse refuse to start if you try to enable more than one of CAS/OAuth/SAML2.
@@ -1367,6 +1367,18 @@ saml2_config: | |||
# # name: value | |||
|
|||
|
|||
|
|||
# Enable OAuth2 for registration and login. |
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.
please don't follow the poor example of the CAS
config block: there are conventions defined for the config file at https://github.com/matrix-org/synapse/blob/master/docs/code_style.md#configuration-file-format.
|
||
|
||
class OAuth2ResponseServlet(RestServlet): | ||
PATTERNS = client_patterns("/login/oauth/response", v1=True) |
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.
once again, the CAS code sets a poor example here. This doesn't form part of the matrix client-server API, so it should be on a different path. Have a look at the way the SAML2ResponseResource
works.
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 pressed the wrong button ^
Thank you for the review. I will improve my code and come back to you once I finished the fixes or have additional questions. |
Hey @maxklenk. I started working on kinda the same thing this week (implementing OpenID Connect, which is ~basically OAuth2) and did not saw this PR before working on it. Do you mind if continue my work and submit a PR by the end of the week? It should also cover your use case, but be a lot more flexible |
The main things I don't really know what the right approach is, is how to carry state throughout the login flow. @richvdh I'd like your input on this. Would it be OK to store a cookie through the flow to save some infos about it? Maybe a macaroon token with those infos in them might be a good candidate for that Also, there are multiple approach to get the user infos:
Ideally, all of those should be supported. The scopes should be configurable, as well as the claims (~user attributes) are mapped. The OpenID Connect spec also specifies a way to logout from Relying Parties (in this case Synapse), initiated by the provider. This implies that we store the token we get from the provider so we can revoke the matrix access token when the user logs out from the OIDC Provider |
).encode("ascii") | ||
return b"%s?%s" % (self.oauth2_server_authorization_url, service_param) | ||
|
||
async def handle_oauth2_response(self, request): |
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.
This should also handle oauth2 errors
""" | ||
|
||
oauth2_nonce = random_string(12) | ||
self.nonces[oauth2_nonce] = {"redirectUrl": client_redirect_url} |
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 nonce
and the state
fields should be two different things and should be checked somehow when the clients return (usually with a cookie). Also, I'm not sure if storing that in memory is a good idea, as it means this servlet can't be scaled, and restarting it will make all the running auth flows fail.
userinfo = await self.get_userinfo(access_token) | ||
|
||
user = "sso_" + userinfo.get("sub") | ||
displayname = userinfo.get("preferred_username") |
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.
That should not be hard-coded. Also, only the sub
field is guaranteed to be here.
Interesting. We've not needed this for CAS or SAML (for SAML there is an In principle though, there is no problem with storing a cookie for the duration of the login process. |
The See https://auth0.com/docs/protocols/oauth2/oauth-state
It is also recommended to have the cookie saving the state signed to prevent it from being forged. What would you think of using a macaroon token, with the |
Thanks for doing some additional testing! 👍 |
It is now possible to register and login in using the OAuth2 Authorization Code Flow.
Things to discuss:
Pull Request Checklist
EventStore
toEventWorkerStore
.".code blocks
.Signed-off-by: Max Klenk max@klenk.biz