Skip to content
Snippets Groups Projects
Unverified Commit c543bf87 authored by Richard van der Hoff's avatar Richard van der Hoff Committed by GitHub
Browse files

Collect terms consent from the user during SSO registration (#9276)

parent e5d70c8a
No related branches found
No related tags found
No related merge requests found
Improve the user experience of setting up an account via single-sign on.
...@@ -2003,6 +2003,28 @@ sso: ...@@ -2003,6 +2003,28 @@ sso:
# #
# * username: the localpart of the user's chosen user id # * username: the localpart of the user's chosen user id
# #
# * HTML page allowing the user to consent to the server's terms and
# conditions. This is only shown for new users, and only if
# `user_consent.require_at_registration` is set.
#
# When rendering, this template is given the following variables:
#
# * server_name: the homeserver's name.
#
# * user_id: the user's matrix proposed ID.
#
# * user_profile.display_name: the user's proposed display name, if any.
#
# * consent_version: the version of the terms that the user will be
# shown
#
# * terms_url: a link to the page showing the terms.
#
# The template should render a form which submits the following fields:
#
# * accepted_version: the version of the terms accepted by the user
# (ie, 'consent_version' from the input variables).
#
# * HTML page for a confirmation step before redirecting back to the client # * HTML page for a confirmation step before redirecting back to the client
# with the login token: 'sso_redirect_confirm.html'. # with the login token: 'sso_redirect_confirm.html'.
# #
......
...@@ -259,6 +259,7 @@ using): ...@@ -259,6 +259,7 @@ using):
^/_matrix/client/(api/v1|r0|unstable)/login/sso/redirect ^/_matrix/client/(api/v1|r0|unstable)/login/sso/redirect
^/_synapse/client/pick_idp$ ^/_synapse/client/pick_idp$
^/_synapse/client/pick_username ^/_synapse/client/pick_username
^/_synapse/client/new_user_consent$
^/_synapse/client/sso_register$ ^/_synapse/client/sso_register$
# OpenID Connect requests. # OpenID Connect requests.
......
...@@ -158,6 +158,28 @@ class SSOConfig(Config): ...@@ -158,6 +158,28 @@ class SSOConfig(Config):
# #
# * username: the localpart of the user's chosen user id # * username: the localpart of the user's chosen user id
# #
# * HTML page allowing the user to consent to the server's terms and
# conditions. This is only shown for new users, and only if
# `user_consent.require_at_registration` is set.
#
# When rendering, this template is given the following variables:
#
# * server_name: the homeserver's name.
#
# * user_id: the user's matrix proposed ID.
#
# * user_profile.display_name: the user's proposed display name, if any.
#
# * consent_version: the version of the terms that the user will be
# shown
#
# * terms_url: a link to the page showing the terms.
#
# The template should render a form which submits the following fields:
#
# * accepted_version: the version of the terms accepted by the user
# (ie, 'consent_version' from the input variables).
#
# * HTML page for a confirmation step before redirecting back to the client # * HTML page for a confirmation step before redirecting back to the client
# with the login token: 'sso_redirect_confirm.html'. # with the login token: 'sso_redirect_confirm.html'.
# #
......
...@@ -694,6 +694,8 @@ class RegistrationHandler(BaseHandler): ...@@ -694,6 +694,8 @@ class RegistrationHandler(BaseHandler):
access_token: The access token of the newly logged in device, or access_token: The access token of the newly logged in device, or
None if `inhibit_login` enabled. None if `inhibit_login` enabled.
""" """
# TODO: 3pid registration can actually happen on the workers. Consider
# refactoring it.
if self.hs.config.worker_app: if self.hs.config.worker_app:
await self._post_registration_client( await self._post_registration_client(
user_id=user_id, auth_result=auth_result, access_token=access_token user_id=user_id, auth_result=auth_result, access_token=access_token
......
...@@ -155,6 +155,7 @@ class UsernameMappingSession: ...@@ -155,6 +155,7 @@ class UsernameMappingSession:
chosen_localpart = attr.ib(type=Optional[str], default=None) chosen_localpart = attr.ib(type=Optional[str], default=None)
use_display_name = attr.ib(type=bool, default=True) use_display_name = attr.ib(type=bool, default=True)
emails_to_use = attr.ib(type=Collection[str], default=()) emails_to_use = attr.ib(type=Collection[str], default=())
terms_accepted_version = attr.ib(type=Optional[str], default=None)
# the HTTP cookie used to track the mapping session id # the HTTP cookie used to track the mapping session id
...@@ -190,6 +191,8 @@ class SsoHandler: ...@@ -190,6 +191,8 @@ class SsoHandler:
# map from idp_id to SsoIdentityProvider # map from idp_id to SsoIdentityProvider
self._identity_providers = {} # type: Dict[str, SsoIdentityProvider] self._identity_providers = {} # type: Dict[str, SsoIdentityProvider]
self._consent_at_registration = hs.config.consent.user_consent_at_registration
def register_identity_provider(self, p: SsoIdentityProvider): def register_identity_provider(self, p: SsoIdentityProvider):
p_id = p.idp_id p_id = p.idp_id
assert p_id not in self._identity_providers assert p_id not in self._identity_providers
...@@ -761,6 +764,38 @@ class SsoHandler: ...@@ -761,6 +764,38 @@ class SsoHandler:
) )
session.emails_to_use = filtered_emails session.emails_to_use = filtered_emails
# we may now need to collect consent from the user, in which case, redirect
# to the consent-extraction-unit
if self._consent_at_registration:
redirect_url = b"/_synapse/client/new_user_consent"
# otherwise, redirect to the completion page
else:
redirect_url = b"/_synapse/client/sso_register"
respond_with_redirect(request, redirect_url)
async def handle_terms_accepted(
self, request: Request, session_id: str, terms_version: str
):
"""Handle a request to the new-user 'consent' endpoint
Will serve an HTTP response to the request.
Args:
request: HTTP request
session_id: ID of the username mapping session, extracted from a cookie
terms_version: the version of the terms which the user viewed and consented
to
"""
logger.info(
"[session %s] User consented to terms version %s",
session_id,
terms_version,
)
session = self.get_mapping_session(session_id)
session.terms_accepted_version = terms_version
# we're done; now we can register the user # we're done; now we can register the user
respond_with_redirect(request, b"/_synapse/client/sso_register") respond_with_redirect(request, b"/_synapse/client/sso_register")
...@@ -816,6 +851,15 @@ class SsoHandler: ...@@ -816,6 +851,15 @@ class SsoHandler:
path=b"/", path=b"/",
) )
auth_result = {}
if session.terms_accepted_version:
# TODO: make this less awful.
auth_result[LoginType.TERMS] = True
await self._registration_handler.post_registration_actions(
user_id, auth_result, access_token=None
)
await self._auth_handler.complete_sso_login( await self._auth_handler.complete_sso_login(
user_id, user_id,
request, request,
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSO redirect confirmation</title>
<meta name="viewport" content="width=device-width, user-scalable=no">
<style type="text/css">
{% include "sso.css" without context %}
#consent_form {
margin-top: 56px;
}
</style>
</head>
<body>
<header>
<h1>Your account is nearly ready</h1>
<p>Agree to the terms to create your account.</p>
</header>
<main>
<!-- {% if user_profile.avatar_url and user_profile.display_name %} -->
<div class="profile">
<img src="{{ user_profile.avatar_url | mxc_to_http(64, 64) }}" class="avatar" />
<div class="profile-details">
<div class="display-name">{{ user_profile.display_name }}</div>
<div class="user-id">{{ user_id }}</div>
</div>
</div>
<!-- {% endif %} -->
<form method="post" action="{{my_url}}" id="consent_form">
<p>
<input id="accepted_version" type="checkbox" name="accepted_version" value="{{ consent_version }}" required>
<label for="accepted_version">I have read and agree to the <a href="{{ terms_url }}" target="_blank">terms and conditions</a>.</label>
</p>
<input type="submit" class="primary-button" value="Continue"/>
</form>
</main>
</body>
</html>
...@@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Mapping ...@@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Mapping
from twisted.web.resource import Resource from twisted.web.resource import Resource
from synapse.rest.synapse.client.new_user_consent import NewUserConsentResource
from synapse.rest.synapse.client.pick_idp import PickIdpResource from synapse.rest.synapse.client.pick_idp import PickIdpResource
from synapse.rest.synapse.client.pick_username import pick_username_resource from synapse.rest.synapse.client.pick_username import pick_username_resource
from synapse.rest.synapse.client.sso_register import SsoRegisterResource from synapse.rest.synapse.client.sso_register import SsoRegisterResource
...@@ -39,6 +40,7 @@ def build_synapse_client_resource_tree(hs: "HomeServer") -> Mapping[str, Resourc ...@@ -39,6 +40,7 @@ def build_synapse_client_resource_tree(hs: "HomeServer") -> Mapping[str, Resourc
# enabled (they just won't work very well if it's not) # enabled (they just won't work very well if it's not)
"/_synapse/client/pick_idp": PickIdpResource(hs), "/_synapse/client/pick_idp": PickIdpResource(hs),
"/_synapse/client/pick_username": pick_username_resource(hs), "/_synapse/client/pick_username": pick_username_resource(hs),
"/_synapse/client/new_user_consent": NewUserConsentResource(hs),
"/_synapse/client/sso_register": SsoRegisterResource(hs), "/_synapse/client/sso_register": SsoRegisterResource(hs),
} }
......
# -*- coding: utf-8 -*-
# Copyright 2021 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from typing import TYPE_CHECKING
from twisted.web.http import Request
from synapse.api.errors import SynapseError
from synapse.handlers.sso import get_username_mapping_session_cookie_from_request
from synapse.http.server import DirectServeHtmlResource, respond_with_html
from synapse.http.servlet import parse_string
from synapse.types import UserID
from synapse.util.templates import build_jinja_env
if TYPE_CHECKING:
from synapse.server import HomeServer
logger = logging.getLogger(__name__)
class NewUserConsentResource(DirectServeHtmlResource):
"""A resource which collects consent to the server's terms from a new user
This resource gets mounted at /_synapse/client/new_user_consent, and is shown
when we are automatically creating a new user due to an SSO login.
It shows a template which prompts the user to go and read the Ts and Cs, and click
a clickybox if they have done so.
"""
def __init__(self, hs: "HomeServer"):
super().__init__()
self._sso_handler = hs.get_sso_handler()
self._server_name = hs.hostname
self._consent_version = hs.config.consent.user_consent_version
def template_search_dirs():
if hs.config.sso.sso_template_dir:
yield hs.config.sso.sso_template_dir
yield hs.config.sso.default_template_dir
self._jinja_env = build_jinja_env(template_search_dirs(), hs.config)
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 _async_render_POST(self, request: Request):
try:
session_id = get_username_mapping_session_cookie_from_request(request)
except SynapseError as e:
logger.warning("Error fetching session cookie: %s", e)
self._sso_handler.render_error(request, "bad_session", e.msg, code=e.code)
return
try:
accepted_version = parse_string(request, "accepted_version", required=True)
except SynapseError as e:
self._sso_handler.render_error(request, "bad_param", e.msg, code=e.code)
return
await self._sso_handler.handle_terms_accepted(
request, session_id, accepted_version
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment