Merge branch 'social_login' into develop

This commit is contained in:
Richard van der Hoff 2021-02-01 18:46:12 +00:00
commit 5963426b95
21 changed files with 495 additions and 98 deletions

1
changelog.d/9276.feature Normal file
View file

@ -0,0 +1 @@
Improve the user experience of setting up an account via single-sign on.

1
changelog.d/9277.feature Normal file
View file

@ -0,0 +1 @@
Improve the user experience of setting up an account via single-sign on.

1
changelog.d/9286.feature Normal file
View file

@ -0,0 +1 @@
Improve the user experience of setting up an account via single-sign on.

1
changelog.d/9287.feature Normal file
View file

@ -0,0 +1 @@
Improve the user experience of setting up an account via single-sign on.

View file

@ -1968,8 +1968,13 @@ sso:
# #
# * providers: a list of available Identity Providers. Each element is # * providers: a list of available Identity Providers. Each element is
# an object with the following attributes: # an object with the following attributes:
#
# * idp_id: unique identifier for the IdP # * idp_id: unique identifier for the IdP
# * idp_name: user-facing name for the IdP # * idp_name: user-facing name for the IdP
# * idp_icon: if specified in the IdP config, an MXC URI for an icon
# for the IdP
# * idp_brand: if specified in the IdP config, a textual identifier
# for the brand of the IdP
# #
# The rendered HTML page should contain a form which submits its results # The rendered HTML page should contain a form which submits its results
# back as a GET request, with the following query parameters: # back as a GET request, with the following query parameters:
@ -2008,6 +2013,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'.
# #
@ -2047,6 +2074,16 @@ sso:
# #
# * description: the operation which the user is being asked to confirm # * description: the operation which the user is being asked to confirm
# #
# * idp: details of the Identity Provider that we will use to confirm
# the user's identity: an object with the following attributes:
#
# * idp_id: unique identifier for the IdP
# * idp_name: user-facing name for the IdP
# * idp_icon: if specified in the IdP config, an MXC URI for an icon
# for the IdP
# * idp_brand: if specified in the IdP config, a textual identifier
# for the brand of the IdP
#
# * HTML page shown after a successful user interactive authentication session: # * HTML page shown after a successful user interactive authentication session:
# 'sso_auth_success.html'. # 'sso_auth_success.html'.
# #

View file

@ -262,6 +262,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.

View file

@ -113,8 +113,13 @@ class SSOConfig(Config):
# #
# * providers: a list of available Identity Providers. Each element is # * providers: a list of available Identity Providers. Each element is
# an object with the following attributes: # an object with the following attributes:
#
# * idp_id: unique identifier for the IdP # * idp_id: unique identifier for the IdP
# * idp_name: user-facing name for the IdP # * idp_name: user-facing name for the IdP
# * idp_icon: if specified in the IdP config, an MXC URI for an icon
# for the IdP
# * idp_brand: if specified in the IdP config, a textual identifier
# for the brand of the IdP
# #
# The rendered HTML page should contain a form which submits its results # The rendered HTML page should contain a form which submits its results
# back as a GET request, with the following query parameters: # back as a GET request, with the following query parameters:
@ -153,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'.
# #
@ -192,6 +219,16 @@ class SSOConfig(Config):
# #
# * description: the operation which the user is being asked to confirm # * description: the operation which the user is being asked to confirm
# #
# * idp: details of the Identity Provider that we will use to confirm
# the user's identity: an object with the following attributes:
#
# * idp_id: unique identifier for the IdP
# * idp_name: user-facing name for the IdP
# * idp_icon: if specified in the IdP config, an MXC URI for an icon
# for the IdP
# * idp_brand: if specified in the IdP config, a textual identifier
# for the brand of the IdP
#
# * HTML page shown after a successful user interactive authentication session: # * HTML page shown after a successful user interactive authentication session:
# 'sso_auth_success.html'. # 'sso_auth_success.html'.
# #

View file

@ -1378,7 +1378,9 @@ class AuthHandler(BaseHandler):
) )
return self._sso_auth_confirm_template.render( return self._sso_auth_confirm_template.render(
description=session.description, redirect_url=redirect_url, description=session.description,
redirect_url=redirect_url,
idp=sso_auth_provider,
) )
async def complete_sso_login( async def complete_sso_login(

View file

@ -14,8 +14,9 @@
# limitations under the License. # limitations under the License.
"""Contains functions for registering clients.""" """Contains functions for registering clients."""
import logging import logging
from typing import TYPE_CHECKING, List, Optional, Tuple from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple
from synapse import types from synapse import types
from synapse.api.constants import MAX_USERID_LENGTH, EventTypes, JoinRules, LoginType from synapse.api.constants import MAX_USERID_LENGTH, EventTypes, JoinRules, LoginType
@ -152,7 +153,7 @@ class RegistrationHandler(BaseHandler):
user_type: Optional[str] = None, user_type: Optional[str] = None,
default_display_name: Optional[str] = None, default_display_name: Optional[str] = None,
address: Optional[str] = None, address: Optional[str] = None,
bind_emails: List[str] = [], bind_emails: Iterable[str] = [],
by_admin: bool = False, by_admin: bool = False,
user_agent_ips: Optional[List[Tuple[str, str]]] = None, user_agent_ips: Optional[List[Tuple[str, str]]] = None,
) -> str: ) -> str:
@ -693,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

View file

@ -14,7 +14,16 @@
# limitations under the License. # limitations under the License.
import abc import abc
import logging import logging
from typing import TYPE_CHECKING, Awaitable, Callable, Dict, List, Mapping, Optional from typing import (
TYPE_CHECKING,
Awaitable,
Callable,
Dict,
Iterable,
Mapping,
Optional,
Set,
)
from urllib.parse import urlencode from urllib.parse import urlencode
import attr import attr
@ -29,7 +38,7 @@ from synapse.handlers.ui_auth import UIAuthSessionDataConstants
from synapse.http import get_request_user_agent from synapse.http import get_request_user_agent
from synapse.http.server import respond_with_html, respond_with_redirect from synapse.http.server import respond_with_html, respond_with_redirect
from synapse.http.site import SynapseRequest from synapse.http.site import SynapseRequest
from synapse.types import JsonDict, UserID, contains_invalid_mxid_characters from synapse.types import Collection, JsonDict, UserID, contains_invalid_mxid_characters
from synapse.util.async_helpers import Linearizer from synapse.util.async_helpers import Linearizer
from synapse.util.stringutils import random_string from synapse.util.stringutils import random_string
@ -115,7 +124,7 @@ class UserAttributes:
# enter one. # enter one.
localpart = attr.ib(type=Optional[str]) localpart = attr.ib(type=Optional[str])
display_name = attr.ib(type=Optional[str], default=None) display_name = attr.ib(type=Optional[str], default=None)
emails = attr.ib(type=List[str], default=attr.Factory(list)) emails = attr.ib(type=Collection[str], default=attr.Factory(list))
@attr.s(slots=True) @attr.s(slots=True)
@ -130,7 +139,7 @@ class UsernameMappingSession:
# attributes returned by the ID mapper # attributes returned by the ID mapper
display_name = attr.ib(type=Optional[str]) display_name = attr.ib(type=Optional[str])
emails = attr.ib(type=List[str]) emails = attr.ib(type=Collection[str])
# An optional dictionary of extra attributes to be provided to the client in the # An optional dictionary of extra attributes to be provided to the client in the
# login response. # login response.
@ -144,6 +153,9 @@ class UsernameMappingSession:
# choices made by the user # choices made by the user
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)
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
@ -179,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
@ -710,7 +724,12 @@ class SsoHandler:
return not user_infos return not user_infos
async def handle_submit_username_request( async def handle_submit_username_request(
self, request: SynapseRequest, localpart: str, session_id: str self,
request: SynapseRequest,
session_id: str,
localpart: str,
use_display_name: bool,
emails_to_use: Iterable[str],
) -> None: ) -> None:
"""Handle a request to the username-picker 'submit' endpoint """Handle a request to the username-picker 'submit' endpoint
@ -720,11 +739,62 @@ class SsoHandler:
request: HTTP request request: HTTP request
localpart: localpart requested by the user localpart: localpart requested by the user
session_id: ID of the username mapping session, extracted from a cookie session_id: ID of the username mapping session, extracted from a cookie
use_display_name: whether the user wants to use the suggested display name
emails_to_use: emails that the user would like to use
""" """
session = self.get_mapping_session(session_id) session = self.get_mapping_session(session_id)
# update the session with the user's choices # update the session with the user's choices
session.chosen_localpart = localpart session.chosen_localpart = localpart
session.use_display_name = use_display_name
emails_from_idp = set(session.emails)
filtered_emails = set() # type: Set[str]
# we iterate through the list rather than just building a set conjunction, so
# that we can log attempts to use unknown addresses
for email in emails_to_use:
if email in emails_from_idp:
filtered_emails.add(email)
else:
logger.warning(
"[session %s] ignoring user request to use unknown email address %r",
session_id,
email,
)
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")
@ -747,11 +817,12 @@ class SsoHandler:
) )
attributes = UserAttributes( attributes = UserAttributes(
localpart=session.chosen_localpart, localpart=session.chosen_localpart, emails=session.emails_to_use,
display_name=session.display_name,
emails=session.emails,
) )
if session.use_display_name:
attributes.display_name = session.display_name
# the following will raise a 400 error if the username has been taken in the # the following will raise a 400 error if the username has been taken in the
# meantime. # meantime.
user_id = await self._register_mapped_user( user_id = await self._register_mapped_user(
@ -780,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,

View file

@ -20,6 +20,10 @@ h1 {
font-size: 24px; font-size: 24px;
} }
.error_page h1 {
color: #FE2928;
}
h2 { h2 {
font-size: 14px; font-size: 14px;
} }
@ -51,6 +55,7 @@ main {
display: block; display: block;
border-radius: 12px; border-radius: 12px;
width: 100%; width: 100%;
box-sizing: border-box;
margin: 16px 0; margin: 16px 0;
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
@ -80,4 +85,4 @@ main {
.profile .display-name, .profile .user-id { .profile .display-name, .profile .user-id {
line-height: 18px; line-height: 18px;
} }

View file

@ -1,10 +1,24 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>SSO account deactivated</title> <title>SSO account deactivated</title>
</head> <meta name="viewport" content="width=device-width, user-scalable=no">
<body> <style type="text/css">
<p>This account has been deactivated.</p> {% include "sso.css" without context %}
</style>
</head>
<body class="error_page">
<header>
<h1>Your account has been deactivated</h1>
<p>
<strong>No account found</strong>
</p>
<p>
Your account might have been deactivated by the server administrator.
You can either try to create a new account or contact the servers
administrator.
</p>
</header>
</body> </body>
</html> </html>

View file

@ -53,6 +53,14 @@
border-top: 1px solid #E9ECF1; border-top: 1px solid #E9ECF1;
padding: 12px; padding: 12px;
} }
.idp-pick-details .check-row {
display: flex;
align-items: center;
}
.idp-pick-details .check-row .name {
flex: 1;
}
.idp-pick-details .use, .idp-pick-details .idp-value { .idp-pick-details .use, .idp-pick-details .idp-value {
color: #737D8C; color: #737D8C;
@ -91,16 +99,31 @@
<h2><img src="{{ idp.idp_icon | mxc_to_http(24, 24) }}"/>Information from {{ idp.idp_name }}</h2> <h2><img src="{{ idp.idp_icon | mxc_to_http(24, 24) }}"/>Information from {{ idp.idp_name }}</h2>
{% if user_attributes.avatar_url %} {% if user_attributes.avatar_url %}
<div class="idp-detail idp-avatar"> <div class="idp-detail idp-avatar">
<div class="check-row">
<label for="idp-avatar" class="name">Avatar</label>
<label for="idp-avatar" class="use">Use</label>
<input type="checkbox" name="use_avatar" id="idp-avatar" value="true" checked>
</div>
<img src="{{ user_attributes.avatar_url }}" class="avatar" /> <img src="{{ user_attributes.avatar_url }}" class="avatar" />
</div> </div>
{% endif %} {% endif %}
{% if user_attributes.display_name %} {% if user_attributes.display_name %}
<div class="idp-detail"> <div class="idp-detail">
<div class="check-row">
<label for="idp-displayname" class="name">Display name</label>
<label for="idp-displayname" class="use">Use</label>
<input type="checkbox" name="use_display_name" id="idp-displayname" value="true" checked>
</div>
<p class="idp-value">{{ user_attributes.display_name }}</p> <p class="idp-value">{{ user_attributes.display_name }}</p>
</div> </div>
{% endif %} {% endif %}
{% for email in user_attributes.emails %} {% for email in user_attributes.emails %}
<div class="idp-detail"> <div class="idp-detail">
<div class="check-row">
<label for="idp-email{{ loop.index }}" class="name">E-mail</label>
<label for="idp-email{{ loop.index }}" class="use">Use</label>
<input type="checkbox" name="use_email" id="idp-email{{ loop.index }}" value="{{ email }}" checked>
</div>
<p class="idp-value">{{ email }}</p> <p class="idp-value">{{ email }}</p>
</div> </div>
{% endfor %} {% endfor %}

View file

@ -1,18 +1,25 @@
<html> <!DOCTYPE html>
<head> <html lang="en">
<title>Authentication Failed</title> <head>
</head> <meta charset="UTF-8">
<body> <title>Authentication failed</title>
<div> <meta name="viewport" content="width=device-width, user-scalable=no">
<style type="text/css">
{% include "sso.css" without context %}
</style>
</head>
<body class="error_page">
<header>
<h1>That doesn't look right</h1>
<p> <p>
We were unable to validate your <tt>{{ server_name }}</tt> account via <strong>We were unable to validate your {{ server_name }} account</strong>
single-sign-on (SSO), because the SSO Identity Provider returned via single&nbsp;sign&#8209;on&nbsp;(SSO), because the SSO Identity
different details than when you logged in. Provider returned different details than when you logged in.
</p> </p>
<p> <p>
Try the operation again, and ensure that you use the same details on Try the operation again, and ensure that you use the same details on
the Identity Provider as when you log into your account. the Identity Provider as when you log into your account.
</p> </p>
</div> </header>
</body> </body>
</html> </html>

View file

@ -1,14 +1,28 @@
<html> <!DOCTYPE html>
<head> <html lang="en">
<title>Authentication</title> <head>
</head> <meta charset="UTF-8">
<title>Authentication</title>
<meta name="viewport" content="width=device-width, user-scalable=no">
<style type="text/css">
{% include "sso.css" without context %}
</style>
</head>
<body> <body>
<div> <header>
<h1>Confirm it's you to continue</h1>
<p> <p>
A client is trying to {{ description }}. To confirm this action, A client is trying to {{ description }}. To confirm this action
<a href="{{ redirect_url }}">re-authenticate with single sign-on</a>. re-authorize your account with single sign-on.
If you did not expect this, your account may be compromised!
</p> </p>
</div> <p><strong>
If you did not expect this, your account may be compromised.
</strong></p>
</header>
<main>
<a href="{{ redirect_url }}" class="primary-button"/>
Continue with {{ idp.idp_name }}
</a>
</main>
</body> </body>
</html> </html>

View file

@ -1,18 +1,27 @@
<html> <!DOCTYPE html>
<head> <html lang="en">
<title>Authentication Successful</title> <head>
<script> <meta charset="UTF-8">
if (window.onAuthDone) { <title>Authentication successful</title>
window.onAuthDone(); <meta name="viewport" content="width=device-width, user-scalable=no">
} else if (window.opener && window.opener.postMessage) { <style type="text/css">
window.opener.postMessage("authDone", "*"); {% include "sso.css" without context %}
} </style>
</script> <script>
</head> if (window.onAuthDone) {
window.onAuthDone();
} else if (window.opener && window.opener.postMessage) {
window.opener.postMessage("authDone", "*");
}
</script>
</head>
<body> <body>
<div> <header>
<p>Thank you</p> <h1>Thank you</h1>
<p>You may now close this window and return to the application</p> <p>
</div> Now we know its you, you can close this window and return to the
application.
</p>
</header>
</body> </body>
</html> </html>

View file

@ -1,53 +1,68 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>SSO error</title> <title>Authentication failed</title>
</head> <meta name="viewport" content="width=device-width, user-scalable=no">
<body> <style type="text/css">
{% include "sso.css" without context %}
#error_code {
margin-top: 56px;
}
</style>
</head>
<body class="error_page">
{# If an error of unauthorised is returned it means we have actively rejected their login #} {# If an error of unauthorised is returned it means we have actively rejected their login #}
{% if error == "unauthorised" %} {% if error == "unauthorised" %}
<p>You are not allowed to log in here.</p> <header>
<p>You are not allowed to log in here.</p>
</header>
{% else %} {% else %}
<p> <header>
There was an error during authentication: <h1>There was an error</h1>
</p> <p>
<div id="errormsg" style="margin:20px 80px">{{ error_description }}</div> <strong id="errormsg">{{ error_description }}</strong>
<p> </p>
If you are seeing this page after clicking a link sent to you via email, make <p>
sure you only click the confirmation link once, and that you open the If you are seeing this page after clicking a link sent to you via email,
validation link in the same client you're logging in from. make sure you only click the confirmation link once, and that you open
</p> the validation link in the same client you're logging in from.
<p> </p>
Try logging in again from your Matrix client and if the problem persists <p>
please contact the server's administrator. Try logging in again from your Matrix client and if the problem persists
</p> please contact the server's administrator.
<p>Error: <code>{{ error }}</code></p> </p>
<div id="error_code">
<p><strong>Error code</strong></p>
<p>{{ error }}</p>
</div>
</header>
<script type="text/javascript"> <script type="text/javascript">
// Error handling to support Auth0 errors that we might get through a GET request // Error handling to support Auth0 errors that we might get through a GET request
// to the validation endpoint. If an error is provided, it's either going to be // to the validation endpoint. If an error is provided, it's either going to be
// located in the query string or in a query string-like URI fragment. // located in the query string or in a query string-like URI fragment.
// We try to locate the error from any of these two locations, but if we can't // We try to locate the error from any of these two locations, but if we can't
// we just don't print anything specific. // we just don't print anything specific.
let searchStr = ""; let searchStr = "";
if (window.location.search) { if (window.location.search) {
// window.location.searchParams isn't always defined when // window.location.searchParams isn't always defined when
// window.location.search is, so it's more reliable to parse the latter. // window.location.search is, so it's more reliable to parse the latter.
searchStr = window.location.search; searchStr = window.location.search;
} else if (window.location.hash) { } else if (window.location.hash) {
// Replace the # with a ? so that URLSearchParams does the right thing and // Replace the # with a ? so that URLSearchParams does the right thing and
// doesn't parse the first parameter incorrectly. // doesn't parse the first parameter incorrectly.
searchStr = window.location.hash.replace("#", "?"); searchStr = window.location.hash.replace("#", "?");
} }
// We might end up with no error in the URL, so we need to check if we have one // We might end up with no error in the URL, so we need to check if we have one
// to print one. // to print one.
let errorDesc = new URLSearchParams(searchStr).get("error_description") let errorDesc = new URLSearchParams(searchStr).get("error_description")
if (errorDesc) { if (errorDesc) {
document.getElementById("errormsg").innerText = errorDesc; document.getElementById("errormsg").innerText = errorDesc;
} }
</script> </script>
{% endif %} {% endif %}
</body> </body>
</html> </html>

View file

@ -0,0 +1,39 @@
<!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>

View file

@ -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
# 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),
} }

View file

@ -0,0 +1,97 @@
# -*- 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
)

View file

@ -14,7 +14,7 @@
# limitations under the License. # limitations under the License.
import logging import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, List
from twisted.web.http import Request from twisted.web.http import Request
from twisted.web.resource import Resource from twisted.web.resource import Resource
@ -26,7 +26,7 @@ from synapse.http.server import (
DirectServeJsonResource, DirectServeJsonResource,
respond_with_html, respond_with_html,
) )
from synapse.http.servlet import parse_string from synapse.http.servlet import parse_boolean, parse_string
from synapse.http.site import SynapseRequest from synapse.http.site import SynapseRequest
from synapse.util.templates import build_jinja_env from synapse.util.templates import build_jinja_env
@ -113,11 +113,19 @@ class AccountDetailsResource(DirectServeHtmlResource):
try: try:
localpart = parse_string(request, "username", required=True) localpart = parse_string(request, "username", required=True)
use_display_name = parse_boolean(request, "use_display_name", default=False)
try:
emails_to_use = [
val.decode("utf-8") for val in request.args.get(b"use_email", [])
] # type: List[str]
except ValueError:
raise SynapseError(400, "Query parameter use_email must be utf-8")
except SynapseError as e: except SynapseError as e:
logger.warning("[session %s] bad param: %s", session_id, e) logger.warning("[session %s] bad param: %s", session_id, e)
self._sso_handler.render_error(request, "bad_param", e.msg, code=e.code) self._sso_handler.render_error(request, "bad_param", e.msg, code=e.code)
return return
await self._sso_handler.handle_submit_username_request( await self._sso_handler.handle_submit_username_request(
request, localpart, session_id request, session_id, localpart, use_display_name, emails_to_use
) )