async def _async_render_GET(self, request): """On GET requests on /renew, retrieve the given renewal token from the request query parameters and, if it matches with an account, renew the account. """ if b"token" not in request.args: raise SynapseError(400, "Missing renewal token") renewal_token = request.args[b"token"][0].decode("utf-8") ( token_valid, token_stale, expiration_ts, ) = await self.renew_account(renewal_token) if token_valid: status_code = 200 response = self._account_renewed_template.render( expiration_ts=expiration_ts) elif token_stale: status_code = 200 response = self._account_previously_renewed_template.render( expiration_ts=expiration_ts) else: status_code = 404 response = self._invalid_token_template.render() respond_with_html(request, status_code, response)
async def on_GET(self, request, stagetype): session = parse_string(request, "session") if not session: raise SynapseError(400, "No session supplied") if stagetype == LoginType.RECAPTCHA: html = self.recaptcha_template.render( session=session, myurl="%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), sitekey=self.hs.config.recaptcha_public_key, ) elif stagetype == LoginType.TERMS: html = self.terms_template.render( session=session, terms_url="%s_matrix/consent?v=%s" % (self.hs.config.public_baseurl, self.hs.config.user_consent_version), myurl="%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.TERMS), ) elif stagetype == LoginType.SSO: # Display a confirmation page which prompts the user to # re-authenticate with their SSO provider. html = await self.auth_handler.start_sso_ui_auth(request, session) else: raise SynapseError(404, "Unknown auth stage type") # Render the HTML and return. respond_with_html(request, 200, html) return None
async def _async_render_GET(self, request: Request) -> None: try: session_id = get_username_mapping_session_cookie_from_request( request) session = self._sso_handler.get_mapping_session(session_id) except SynapseError as e: logger.warning("Error fetching session: %s", e) self._sso_handler.render_error(request, "bad_session", e.msg, code=e.code) return user_id = UserID(session.chosen_localpart, self._server_name) user_profile = { "display_name": session.display_name, } template_params = { "user_id": user_id.to_string(), "user_profile": user_profile, "consent_version": self._consent_version, "terms_url": "/_matrix/consent?v=%s" % (self._consent_version, ), } template = self._jinja_env.get_template("sso_new_user_consent.html") html = template.render(template_params) respond_with_html(request, 200, html)
async def complete_sso_login( self, registered_user_id: str, request: SynapseRequest, client_redirect_url: str, ): """Having figured out a mxid for this user, complete the HTTP request Args: registered_user_id: The registered user ID to complete SSO login for. request: The request to complete. client_redirect_url: The URL to which to redirect the user at the end of the process. """ # If the account has been deactivated, do not proceed with the login # flow. deactivated = await self.store.get_user_deactivated_status( registered_user_id) if deactivated: respond_with_html(request, 403, self._sso_account_deactivated_template) return self._complete_sso_login(registered_user_id, request, client_redirect_url)
async def on_GET(self, request): if b"token" not in request.args: raise SynapseError(400, "Missing renewal token") renewal_token = request.args[b"token"][0] ( token_valid, token_stale, expiration_ts, ) = await self.account_activity_handler.renew_account( renewal_token.decode("utf8")) if token_valid: status_code = 200 response = self.account_renewed_template.render( expiration_ts=expiration_ts) elif token_stale: status_code = 200 response = self.account_previously_renewed_template.render( expiration_ts=expiration_ts) else: status_code = 404 response = self.invalid_token_template.render( expiration_ts=expiration_ts) respond_with_html(request, status_code, response)
async def _async_render_GET(self, request: Request) -> None: try: session_id = get_username_mapping_session_cookie_from_request( request) session = self._sso_handler.get_mapping_session(session_id) except SynapseError as e: logger.warning("Error fetching session: %s", e) self._sso_handler.render_error(request, "bad_session", e.msg, code=e.code) return # The configuration might mandate going through this step to validate an # automatically generated localpart, so session.chosen_localpart might already # be set. localpart = "" if session.chosen_localpart is not None: localpart = session.chosen_localpart idp_id = session.auth_provider_id template_params = { "idp": self._sso_handler.get_identity_providers()[idp_id], "user_attributes": { "display_name": session.display_name, "emails": session.emails, "localpart": localpart, }, } template = self._jinja_env.get_template( "sso_auth_account_details.html") html = template.render(template_params) respond_with_html(request, 200, html)
async def _async_render_GET(self, request: Request) -> None: try: session_id = get_username_mapping_session_cookie_from_request( request) session = self._sso_handler.get_mapping_session(session_id) except SynapseError as e: logger.warning("Error fetching session: %s", e) self._sso_handler.render_error(request, "bad_session", e.msg, code=e.code) return idp_id = session.auth_provider_id template_params = { "idp": self._sso_handler.get_identity_providers()[idp_id], "user_attributes": { "display_name": session.display_name, "emails": session.emails, }, } template = self._jinja_env.get_template( "sso_auth_account_details.html") html = template.render(template_params) respond_with_html(request, 200, html)
def _render_template(self, request, template_name, **template_args): # get_template checks for ".." so we don't need to worry too much # about path traversal here. template_html = self._jinja_env.get_template( path.join(TEMPLATE_LANGUAGE, template_name)) html = template_html.render(**template_args) respond_with_html(request, 200, html)
async def on_GET(self, request, stagetype): session = parse_string(request, "session") if not session: raise SynapseError(400, "No session supplied") if stagetype == LoginType.RECAPTCHA: html = RECAPTCHA_TEMPLATE % { "session": session, "myurl": "%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), "sitekey": self.hs.config.recaptcha_public_key, } elif stagetype == LoginType.TERMS: html = TERMS_TEMPLATE % { "session": session, "terms_url": "%s_matrix/consent?v=%s" % (self.hs.config.public_baseurl, self.hs.config.user_consent_version), "myurl": "%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.TERMS), } elif stagetype == LoginType.SSO: # Display a confirmation page which prompts the user to # re-authenticate with their SSO provider. if self._cas_enabled: # Generate a request to CAS that redirects back to an endpoint # to verify the successful authentication. sso_redirect_url = self._cas_handler.get_redirect_url( {"session": session}, ) elif self._saml_enabled: # Some SAML identity providers (e.g. Google) require a # RelayState parameter on requests. It is not necessary here, so # pass in a dummy redirect URL (which will never get used). client_redirect_url = b"unused" sso_redirect_url = self._saml_handler.handle_redirect_request( client_redirect_url, session ) elif self._oidc_enabled: client_redirect_url = b"" sso_redirect_url = await self._oidc_handler.handle_redirect_request( request, client_redirect_url, session ) else: raise SynapseError(400, "Homeserver not configured for SSO.") html = await self.auth_handler.start_sso_ui_auth(sso_redirect_url, session) else: raise SynapseError(404, "Unknown auth stage type") # Render the HTML and return. respond_with_html(request, 200, html) return None
async def on_POST(self, request, stagetype): session = parse_string(request, "session") if not session: raise SynapseError(400, "No session supplied") if stagetype == LoginType.RECAPTCHA: response = parse_string(request, "g-recaptcha-response") if not response: raise SynapseError(400, "No captcha response supplied") authdict = {"response": response, "session": session} success = await self.auth_handler.add_oob_auth( LoginType.RECAPTCHA, authdict, self.hs.get_ip_from_request(request)) if success: html = self.success_template.render() else: html = self.recaptcha_template.render( session=session, myurl="%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), sitekey=self.hs.config.recaptcha_public_key, ) elif stagetype == LoginType.TERMS: authdict = {"session": session} success = await self.auth_handler.add_oob_auth( LoginType.TERMS, authdict, self.hs.get_ip_from_request(request)) if success: html = self.success_template.render() else: html = self.terms_template.render( session=session, terms_url="%s_matrix/consent?v=%s" % ( self.hs.config.public_baseurl, self.hs.config.user_consent_version, ), myurl="%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.TERMS), ) elif stagetype == LoginType.SSO: # The SSO fallback workflow should not post here, raise SynapseError( 404, "Fallback SSO auth does not support POST requests.") else: raise SynapseError(404, "Unknown auth stage type") # Render the HTML and return. respond_with_html(request, 200, html) return None
async def _serve_id_picker(self, request: SynapseRequest, client_redirect_url: str) -> None: # otherwise, serve up the IdP picker providers = self._sso_handler.get_identity_providers() html = self._sso_login_idp_picker_template.render( redirect_url=client_redirect_url, server_name=self._server_name, providers=providers.values(), ) respond_with_html(request, 200, html)
async def on_GET(self, request): if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: logger.warning( "Adding emails have been disabled due to lack of an email config" ) raise SynapseError( 400, "Adding an email to your account is disabled on this server" ) elif self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: raise SynapseError( 400, "This homeserver is not validating threepids. Use an identity server " "instead.", ) sid = parse_string(request, "sid", required=True) token = parse_string(request, "token", required=True) client_secret = parse_string(request, "client_secret", required=True) assert_valid_client_secret(client_secret) # Attempt to validate a 3PID session try: # Mark the session as valid next_link = await self.store.validate_threepid_session( sid, client_secret, token, self.clock.time_msec() ) # Perform a 302 redirect if next_link is set if next_link: if next_link.startswith("file:///"): logger.warning( "Not redirecting to next_link as it is a local file: address" ) else: request.setResponseCode(302) request.setHeader("Location", next_link) finish_request(request) return None # Otherwise show the success template html = self.config.email_add_threepid_template_success_html_content status_code = 200 except ThreepidValidationError as e: status_code = e.code # Show a failure page with a reason template_vars = {"failure_reason": e.msg} html = self._failure_email_template.render(**template_vars) respond_with_html(request, status_code, html)
async def on_GET(self, request, medium): # We currently only handle threepid token submissions for email if medium != "email": raise SynapseError( 400, "This medium is currently not supported for password resets" ) if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: logger.warning( "Password reset emails have been disabled due to lack of an email config" ) raise SynapseError( 400, "Email-based password resets are disabled on this server" ) sid = parse_string(request, "sid", required=True) token = parse_string(request, "token", required=True) client_secret = parse_string(request, "client_secret", required=True) assert_valid_client_secret(client_secret) # Attempt to validate a 3PID session try: # Mark the session as valid next_link = await self.store.validate_threepid_session( sid, client_secret, token, self.clock.time_msec() ) # Perform a 302 redirect if next_link is set if next_link: if next_link.startswith("file:///"): logger.warning( "Not redirecting to next_link as it is a local file: address" ) else: request.setResponseCode(302) request.setHeader("Location", next_link) finish_request(request) return None # Otherwise show the success template html = self.config.email_password_reset_template_success_html_content status_code = 200 except ThreepidValidationError as e: status_code = e.code # Show a failure page with a reason template_vars = {"failure_reason": e.msg} html = self._failure_email_template.render(**template_vars) respond_with_html(request, status_code, html)
async def on_GET(self, request): if b"token" not in request.args: raise SynapseError(400, "Missing renewal token") renewal_token = request.args[b"token"][0] token_valid = await self.account_activity_handler.renew_account( renewal_token.decode("utf8")) if token_valid: status_code = 200 response = self.success_html else: status_code = 404 response = self.failure_html respond_with_html(request, status_code, response)
def _render_error(self, request, error: str, error_description: Optional[str] = None) -> None: """Render the error template and respond to the request with it. This is used to show errors to the user. The template of this page can be found under `synapse/res/templates/sso_error.html`. Args: request: The incoming request from the browser. We'll respond with an HTML page describing the error. error: A technical identifier for this error. error_description: A human-readable description of the error. """ html = self._error_template.render(error=error, error_description=error_description) respond_with_html(request, 400, html)
def _complete_sso_login( self, registered_user_id: str, request: SynapseRequest, client_redirect_url: str, ): """ The synchronous portion of complete_sso_login. This exists purely for backwards compatibility of synapse.module_api.ModuleApi. """ # Create a login token login_token = self.macaroon_gen.generate_short_term_login_token( registered_user_id ) # Append the login token to the original redirect URL (i.e. with its query # parameters kept intact) to build the URL to which the template needs to # redirect the users once they have clicked on the confirmation link. redirect_url = self.add_query_param_to_url( client_redirect_url, "loginToken", login_token ) # if the client is whitelisted, we can redirect straight to it if client_redirect_url.startswith(self._whitelisted_sso_clients): request.redirect(redirect_url) finish_request(request) return # Otherwise, serve the redirect confirmation page. # Remove the query parameters from the redirect URL to get a shorter version of # it. This is only to display a human-readable URL in the template, but not the # URL we redirect users to. redirect_url_no_params = client_redirect_url.split("?")[0] html = self._sso_redirect_confirm_template.render( display_url=redirect_url_no_params, redirect_url=redirect_url, server_name=self._server_name, ) respond_with_html(request, 200, html)
async def complete_sso_ui_auth( self, registered_user_id: str, session_id: str, request: SynapseRequest, ): """Having figured out a mxid for this user, complete the HTTP request Args: registered_user_id: The registered user ID to complete SSO login for. request: The request to complete. client_redirect_url: The URL to which to redirect the user at the end of the process. """ # Mark the stage of the authentication as successful. # Save the user who authenticated with SSO, this will be used to ensure # that the account be modified is also the person who logged in. await self.store.mark_ui_auth_stage_complete( session_id, LoginType.SSO, registered_user_id ) # Render the HTML and return. html = self._sso_auth_success_template respond_with_html(request, 200, html)
async def on_GET(self, request: Request) -> None: renewal_token = parse_string(request, "token", required=True) ( token_valid, token_stale, expiration_ts, ) = await self.account_activity_handler.renew_account(renewal_token) if token_valid: status_code = 200 response = self.account_renewed_template.render(expiration_ts=expiration_ts) elif token_stale: status_code = 200 response = self.account_previously_renewed_template.render( expiration_ts=expiration_ts ) else: status_code = 404 response = self.invalid_token_template.render(expiration_ts=expiration_ts) respond_with_html(request, status_code, response)
async def _async_render_GET(self, request: Request) -> None: try: session_id = get_username_mapping_session_cookie_from_request( request) session = self._sso_handler.get_mapping_session(session_id) except SynapseError as e: logger.warning("Error fetching session: %s", e) self._sso_handler.render_error(request, "bad_session", e.msg, code=e.code) return # It should be impossible to get here without having first been through # the pick-a-username step, which ensures chosen_localpart gets set. if not session.chosen_localpart: logger.warning("Session has no user name selected") self._sso_handler.render_error(request, "no_user", "No user name has been selected.", code=400) return user_id = UserID(session.chosen_localpart, self._server_name) user_profile = { "display_name": session.display_name, } template_params = { "user_id": user_id.to_string(), "user_profile": user_profile, "consent_version": self._consent_version, "terms_url": "/_matrix/consent?v=%s" % (self._consent_version, ), } template = self._jinja_env.get_template("sso_new_user_consent.html") html = template.render(template_params) respond_with_html(request, 200, html)
async def complete_sso_ui_auth_request( self, auth_provider_id: str, remote_user_id: str, ui_auth_session_id: str, request: Request, ) -> None: """ Given an SSO ID, retrieve the user ID for it and complete UIA. Note that this requires that the user is mapped in the "user_external_ids" table. This will be the case if they have ever logged in via SAML or OIDC in recentish synapse versions, but may not be for older users. Args: auth_provider_id: A unique identifier for this SSO provider, e.g. "oidc" or "saml". remote_user_id: The unique identifier from the SSO provider. ui_auth_session_id: The ID of the user-interactive auth session. request: The request to complete. """ user_id = await self.get_sso_user_by_remote_user_id( auth_provider_id, remote_user_id, ) user_id_to_verify = await self._auth_handler.get_session_data( ui_auth_session_id, UIAuthSessionDataConstants.REQUEST_USER_ID) # type: str if not user_id: logger.warning( "Remote user %s/%s has not previously logged in here: UIA will fail", auth_provider_id, remote_user_id, ) elif user_id != user_id_to_verify: logger.warning( "Remote user %s/%s mapped onto incorrect user %s: UIA will fail", auth_provider_id, remote_user_id, user_id, ) else: # success! # Mark the stage of the authentication as successful. await self._store.mark_ui_auth_stage_complete( ui_auth_session_id, LoginType.SSO, user_id) # Render the HTML confirmation page and return. html = self._sso_auth_success_template respond_with_html(request, 200, html) return # the user_id didn't match: mark the stage of the authentication as unsuccessful await self._store.mark_ui_auth_stage_complete(ui_auth_session_id, LoginType.SSO, "") # render an error page. html = self._bad_user_template.render( server_name=self._server_name, user_id_to_verify=user_id_to_verify, ) respond_with_html(request, 200, html)
async def on_POST(self, request: Request, stagetype: str) -> None: session = parse_string(request, "session") if not session: raise SynapseError(400, "No session supplied") if stagetype == LoginType.RECAPTCHA: response = parse_string(request, "g-recaptcha-response") if not response: raise SynapseError(400, "No captcha response supplied") authdict = {"response": response, "session": session} try: await self.auth_handler.add_oob_auth( LoginType.RECAPTCHA, authdict, request.getClientAddress().host) except LoginError as e: # Authentication failed, let user try again html = self.recaptcha_template.render( session=session, myurl="%s/v3/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.RECAPTCHA), sitekey=self.hs.config.captcha.recaptcha_public_key, error=e.msg, ) else: # No LoginError was raised, so authentication was successful html = self.success_template.render() elif stagetype == LoginType.TERMS: authdict = {"session": session} try: await self.auth_handler.add_oob_auth( LoginType.TERMS, authdict, request.getClientAddress().host) except LoginError as e: # Authentication failed, let user try again html = self.terms_template.render( session=session, terms_url="%s_matrix/consent?v=%s" % ( self.hs.config.server.public_baseurl, self.hs.config.consent.user_consent_version, ), myurl="%s/v3/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.TERMS), error=e.msg, ) else: # No LoginError was raised, so authentication was successful html = self.success_template.render() elif stagetype == LoginType.SSO: # The SSO fallback workflow should not post here, raise SynapseError( 404, "Fallback SSO auth does not support POST requests.") elif stagetype == LoginType.REGISTRATION_TOKEN: token = parse_string(request, "token", required=True) authdict = {"session": session, "token": token} try: await self.auth_handler.add_oob_auth( LoginType.REGISTRATION_TOKEN, authdict, request.getClientAddress().host, ) except LoginError as e: html = self.registration_token_template.render( session=session, myurl= f"{CLIENT_API_PREFIX}/r0/auth/{LoginType.REGISTRATION_TOKEN}/fallback/web", error=e.msg, ) else: html = self.success_template.render() else: raise SynapseError(404, "Unknown auth stage type") # Render the HTML and return. respond_with_html(request, 200, html) return None