mirror of
https://github.com/element-hq/synapse.git
synced 2024-11-22 09:35:45 +03:00
Merge branch 'social_login' into develop
This commit is contained in:
commit
5963426b95
21 changed files with 495 additions and 98 deletions
1
changelog.d/9276.feature
Normal file
1
changelog.d/9276.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Improve the user experience of setting up an account via single-sign on.
|
1
changelog.d/9277.feature
Normal file
1
changelog.d/9277.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Improve the user experience of setting up an account via single-sign on.
|
1
changelog.d/9286.feature
Normal file
1
changelog.d/9286.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Improve the user experience of setting up an account via single-sign on.
|
1
changelog.d/9287.feature
Normal file
1
changelog.d/9287.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Improve the user experience of setting up an account via single-sign on.
|
|
@ -1968,8 +1968,13 @@ sso:
|
|||
#
|
||||
# * providers: a list of available Identity Providers. Each element is
|
||||
# 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
|
||||
#
|
||||
# The rendered HTML page should contain a form which submits its results
|
||||
# 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
|
||||
#
|
||||
# * 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
|
||||
# with the login token: 'sso_redirect_confirm.html'.
|
||||
#
|
||||
|
@ -2047,6 +2074,16 @@ sso:
|
|||
#
|
||||
# * 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:
|
||||
# 'sso_auth_success.html'.
|
||||
#
|
||||
|
|
|
@ -262,6 +262,7 @@ using):
|
|||
^/_matrix/client/(api/v1|r0|unstable)/login/sso/redirect
|
||||
^/_synapse/client/pick_idp$
|
||||
^/_synapse/client/pick_username
|
||||
^/_synapse/client/new_user_consent$
|
||||
^/_synapse/client/sso_register$
|
||||
|
||||
# OpenID Connect requests.
|
||||
|
|
|
@ -113,8 +113,13 @@ class SSOConfig(Config):
|
|||
#
|
||||
# * providers: a list of available Identity Providers. Each element is
|
||||
# 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
|
||||
#
|
||||
# The rendered HTML page should contain a form which submits its results
|
||||
# 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
|
||||
#
|
||||
# * 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
|
||||
# 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
|
||||
#
|
||||
# * 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:
|
||||
# 'sso_auth_success.html'.
|
||||
#
|
||||
|
|
|
@ -1378,7 +1378,9 @@ class AuthHandler(BaseHandler):
|
|||
)
|
||||
|
||||
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(
|
||||
|
|
|
@ -14,8 +14,9 @@
|
|||
# limitations under the License.
|
||||
|
||||
"""Contains functions for registering clients."""
|
||||
|
||||
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.api.constants import MAX_USERID_LENGTH, EventTypes, JoinRules, LoginType
|
||||
|
@ -152,7 +153,7 @@ class RegistrationHandler(BaseHandler):
|
|||
user_type: Optional[str] = None,
|
||||
default_display_name: Optional[str] = None,
|
||||
address: Optional[str] = None,
|
||||
bind_emails: List[str] = [],
|
||||
bind_emails: Iterable[str] = [],
|
||||
by_admin: bool = False,
|
||||
user_agent_ips: Optional[List[Tuple[str, str]]] = None,
|
||||
) -> str:
|
||||
|
@ -693,6 +694,8 @@ class RegistrationHandler(BaseHandler):
|
|||
access_token: The access token of the newly logged in device, or
|
||||
None if `inhibit_login` enabled.
|
||||
"""
|
||||
# TODO: 3pid registration can actually happen on the workers. Consider
|
||||
# refactoring it.
|
||||
if self.hs.config.worker_app:
|
||||
await self._post_registration_client(
|
||||
user_id=user_id, auth_result=auth_result, access_token=access_token
|
||||
|
|
|
@ -14,7 +14,16 @@
|
|||
# limitations under the License.
|
||||
import abc
|
||||
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
|
||||
|
||||
import attr
|
||||
|
@ -29,7 +38,7 @@ from synapse.handlers.ui_auth import UIAuthSessionDataConstants
|
|||
from synapse.http import get_request_user_agent
|
||||
from synapse.http.server import respond_with_html, respond_with_redirect
|
||||
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.stringutils import random_string
|
||||
|
||||
|
@ -115,7 +124,7 @@ class UserAttributes:
|
|||
# enter one.
|
||||
localpart = attr.ib(type=Optional[str])
|
||||
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)
|
||||
|
@ -130,7 +139,7 @@ class UsernameMappingSession:
|
|||
|
||||
# attributes returned by the ID mapper
|
||||
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
|
||||
# login response.
|
||||
|
@ -144,6 +153,9 @@ class UsernameMappingSession:
|
|||
|
||||
# choices made by the user
|
||||
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
|
||||
|
@ -179,6 +191,8 @@ class SsoHandler:
|
|||
# map from idp_id to 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):
|
||||
p_id = p.idp_id
|
||||
assert p_id not in self._identity_providers
|
||||
|
@ -710,7 +724,12 @@ class SsoHandler:
|
|||
return not user_infos
|
||||
|
||||
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:
|
||||
"""Handle a request to the username-picker 'submit' endpoint
|
||||
|
||||
|
@ -720,11 +739,62 @@ class SsoHandler:
|
|||
request: HTTP request
|
||||
localpart: localpart requested by the user
|
||||
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)
|
||||
|
||||
# update the session with the user's choices
|
||||
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
|
||||
respond_with_redirect(request, b"/_synapse/client/sso_register")
|
||||
|
@ -747,11 +817,12 @@ class SsoHandler:
|
|||
)
|
||||
|
||||
attributes = UserAttributes(
|
||||
localpart=session.chosen_localpart,
|
||||
display_name=session.display_name,
|
||||
emails=session.emails,
|
||||
localpart=session.chosen_localpart, emails=session.emails_to_use,
|
||||
)
|
||||
|
||||
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
|
||||
# meantime.
|
||||
user_id = await self._register_mapped_user(
|
||||
|
@ -780,6 +851,15 @@ class SsoHandler:
|
|||
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(
|
||||
user_id,
|
||||
request,
|
||||
|
|
|
@ -20,6 +20,10 @@ h1 {
|
|||
font-size: 24px;
|
||||
}
|
||||
|
||||
.error_page h1 {
|
||||
color: #FE2928;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
@ -51,6 +55,7 @@ main {
|
|||
display: block;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 16px 0;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
|
@ -80,4 +85,4 @@ main {
|
|||
|
||||
.profile .display-name, .profile .user-id {
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SSO account deactivated</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>This account has been deactivated.</p>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SSO account deactivated</title>
|
||||
<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>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 server’s
|
||||
administrator.
|
||||
</p>
|
||||
</header>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -53,6 +53,14 @@
|
|||
border-top: 1px solid #E9ECF1;
|
||||
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 {
|
||||
color: #737D8C;
|
||||
|
@ -91,16 +99,31 @@
|
|||
<h2><img src="{{ idp.idp_icon | mxc_to_http(24, 24) }}"/>Information from {{ idp.idp_name }}</h2>
|
||||
{% if user_attributes.avatar_url %}
|
||||
<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" />
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if user_attributes.display_name %}
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for email in user_attributes.emails %}
|
||||
<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>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Authentication Failed</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Authentication failed</title>
|
||||
<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>
|
||||
We were unable to validate your <tt>{{ server_name }}</tt> account via
|
||||
single-sign-on (SSO), because the SSO Identity Provider returned
|
||||
different details than when you logged in.
|
||||
<strong>We were unable to validate your {{ server_name }} account</strong>
|
||||
via single sign‑on (SSO), because the SSO Identity
|
||||
Provider returned different details than when you logged in.
|
||||
</p>
|
||||
<p>
|
||||
Try the operation again, and ensure that you use the same details on
|
||||
the Identity Provider as when you log into your account.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,14 +1,28 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Authentication</title>
|
||||
</head>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<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>
|
||||
<div>
|
||||
<header>
|
||||
<h1>Confirm it's you to continue</h1>
|
||||
<p>
|
||||
A client is trying to {{ description }}. To confirm this action,
|
||||
<a href="{{ redirect_url }}">re-authenticate with single sign-on</a>.
|
||||
If you did not expect this, your account may be compromised!
|
||||
A client is trying to {{ description }}. To confirm this action
|
||||
re-authorize your account with single sign-on.
|
||||
</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>
|
||||
</html>
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Authentication Successful</title>
|
||||
<script>
|
||||
if (window.onAuthDone) {
|
||||
window.onAuthDone();
|
||||
} else if (window.opener && window.opener.postMessage) {
|
||||
window.opener.postMessage("authDone", "*");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Authentication successful</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<style type="text/css">
|
||||
{% include "sso.css" without context %}
|
||||
</style>
|
||||
<script>
|
||||
if (window.onAuthDone) {
|
||||
window.onAuthDone();
|
||||
} else if (window.opener && window.opener.postMessage) {
|
||||
window.opener.postMessage("authDone", "*");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p>Thank you</p>
|
||||
<p>You may now close this window and return to the application</p>
|
||||
</div>
|
||||
<header>
|
||||
<h1>Thank you</h1>
|
||||
<p>
|
||||
Now we know it’s you, you can close this window and return to the
|
||||
application.
|
||||
</p>
|
||||
</header>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,53 +1,68 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SSO error</title>
|
||||
</head>
|
||||
<body>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Authentication failed</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<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 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 %}
|
||||
<p>
|
||||
There was an error during authentication:
|
||||
</p>
|
||||
<div id="errormsg" style="margin:20px 80px">{{ error_description }}</div>
|
||||
<p>
|
||||
If you are seeing this page after clicking a link sent to you via email, make
|
||||
sure you only click the confirmation link once, and that you open 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
|
||||
please contact the server's administrator.
|
||||
</p>
|
||||
<p>Error: <code>{{ error }}</code></p>
|
||||
<header>
|
||||
<h1>There was an error</h1>
|
||||
<p>
|
||||
<strong id="errormsg">{{ error_description }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
If you are seeing this page after clicking a link sent to you via email,
|
||||
make sure you only click the confirmation link once, and that you open
|
||||
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
|
||||
please contact the server's administrator.
|
||||
</p>
|
||||
<div id="error_code">
|
||||
<p><strong>Error code</strong></p>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script type="text/javascript">
|
||||
// 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
|
||||
// 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 just don't print anything specific.
|
||||
let searchStr = "";
|
||||
if (window.location.search) {
|
||||
// window.location.searchParams isn't always defined when
|
||||
// window.location.search is, so it's more reliable to parse the latter.
|
||||
searchStr = window.location.search;
|
||||
} else if (window.location.hash) {
|
||||
// Replace the # with a ? so that URLSearchParams does the right thing and
|
||||
// doesn't parse the first parameter incorrectly.
|
||||
searchStr = window.location.hash.replace("#", "?");
|
||||
}
|
||||
<script type="text/javascript">
|
||||
// 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
|
||||
// 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 just don't print anything specific.
|
||||
let searchStr = "";
|
||||
if (window.location.search) {
|
||||
// window.location.searchParams isn't always defined when
|
||||
// window.location.search is, so it's more reliable to parse the latter.
|
||||
searchStr = window.location.search;
|
||||
} else if (window.location.hash) {
|
||||
// Replace the # with a ? so that URLSearchParams does the right thing and
|
||||
// doesn't parse the first parameter incorrectly.
|
||||
searchStr = window.location.hash.replace("#", "?");
|
||||
}
|
||||
|
||||
// We might end up with no error in the URL, so we need to check if we have one
|
||||
// to print one.
|
||||
let errorDesc = new URLSearchParams(searchStr).get("error_description")
|
||||
if (errorDesc) {
|
||||
document.getElementById("errormsg").innerText = errorDesc;
|
||||
}
|
||||
</script>
|
||||
// We might end up with no error in the URL, so we need to check if we have one
|
||||
// to print one.
|
||||
let errorDesc = new URLSearchParams(searchStr).get("error_description")
|
||||
if (errorDesc) {
|
||||
document.getElementById("errormsg").innerText = errorDesc;
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
39
synapse/res/templates/sso_new_user_consent.html
Normal file
39
synapse/res/templates/sso_new_user_consent.html
Normal 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>
|
|
@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Mapping
|
|||
|
||||
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_username import pick_username_resource
|
||||
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)
|
||||
"/_synapse/client/pick_idp": PickIdpResource(hs),
|
||||
"/_synapse/client/pick_username": pick_username_resource(hs),
|
||||
"/_synapse/client/new_user_consent": NewUserConsentResource(hs),
|
||||
"/_synapse/client/sso_register": SsoRegisterResource(hs),
|
||||
}
|
||||
|
||||
|
|
97
synapse/rest/synapse/client/new_user_consent.py
Normal file
97
synapse/rest/synapse/client/new_user_consent.py
Normal 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
|
||||
)
|
|
@ -14,7 +14,7 @@
|
|||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from twisted.web.http import Request
|
||||
from twisted.web.resource import Resource
|
||||
|
@ -26,7 +26,7 @@ from synapse.http.server import (
|
|||
DirectServeJsonResource,
|
||||
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.util.templates import build_jinja_env
|
||||
|
||||
|
@ -113,11 +113,19 @@ class AccountDetailsResource(DirectServeHtmlResource):
|
|||
|
||||
try:
|
||||
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:
|
||||
logger.warning("[session %s] bad param: %s", session_id, e)
|
||||
self._sso_handler.render_error(request, "bad_param", e.msg, code=e.code)
|
||||
return
|
||||
|
||||
await self._sso_handler.handle_submit_username_request(
|
||||
request, localpart, session_id
|
||||
request, session_id, localpart, use_display_name, emails_to_use
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue