diff --git a/changelog.d/12210.misc b/changelog.d/12210.misc
new file mode 100644
index 0000000000..3f6a8747c2
--- /dev/null
+++ b/changelog.d/12210.misc
@@ -0,0 +1 @@
+Update the SSO username picker template to comply with SIWA guidelines.
diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml
index 6f3623c88a..ef25a3175f 100644
--- a/docs/sample_config.yaml
+++ b/docs/sample_config.yaml
@@ -1947,8 +1947,13 @@ saml2_config:
#
# localpart_template: Jinja2 template for the localpart of the MXID.
# If this is not set, the user will be prompted to choose their
-# own username (see 'sso_auth_account_details.html' in the 'sso'
-# section of this file).
+# own username (see the documentation for the
+# 'sso_auth_account_details.html' template).
+#
+# confirm_localpart: Whether to prompt the user to validate (or
+# change) the generated localpart (see the documentation for the
+# 'sso_auth_account_details.html' template), instead of
+# registering the account right away.
#
# display_name_template: Jinja2 template for the display name to set
# on first login. If unset, no displayname will be set.
diff --git a/docs/templates.md b/docs/templates.md
index 2b66e9d862..b251d05cb9 100644
--- a/docs/templates.md
+++ b/docs/templates.md
@@ -176,8 +176,11 @@ Below are the templates Synapse will look for when generating pages related to S
for the brand of the IdP
* `user_attributes`: an object containing details about the user that
we received from the IdP. May have the following attributes:
- * display_name: the user's display_name
- * emails: a list of email addresses
+ * `display_name`: the user's display name
+ * `emails`: a list of email addresses
+ * `localpart`: the local part of the Matrix user ID to register,
+ if `localpart_template` is set in the mapping provider configuration (empty
+ string if not)
The template should render a form which submits the following fields:
* `username`: the localpart of the user's chosen user id
* `sso_new_user_consent.html`: HTML page allowing the user to consent to the
diff --git a/synapse/config/oidc.py b/synapse/config/oidc.py
index f7e4f9ef22..fc95912d9b 100644
--- a/synapse/config/oidc.py
+++ b/synapse/config/oidc.py
@@ -182,8 +182,13 @@ class OIDCConfig(Config):
#
# localpart_template: Jinja2 template for the localpart of the MXID.
# If this is not set, the user will be prompted to choose their
- # own username (see 'sso_auth_account_details.html' in the 'sso'
- # section of this file).
+ # own username (see the documentation for the
+ # 'sso_auth_account_details.html' template).
+ #
+ # confirm_localpart: Whether to prompt the user to validate (or
+ # change) the generated localpart (see the documentation for the
+ # 'sso_auth_account_details.html' template), instead of
+ # registering the account right away.
#
# display_name_template: Jinja2 template for the display name to set
# on first login. If unset, no displayname will be set.
diff --git a/synapse/handlers/oidc.py b/synapse/handlers/oidc.py
index 593a2aac66..d98659edc7 100644
--- a/synapse/handlers/oidc.py
+++ b/synapse/handlers/oidc.py
@@ -1228,6 +1228,7 @@ class OidcSessionData:
class UserAttributeDict(TypedDict):
localpart: Optional[str]
+ confirm_localpart: bool
display_name: Optional[str]
emails: List[str]
@@ -1316,6 +1317,7 @@ class JinjaOidcMappingConfig:
display_name_template: Optional[Template]
email_template: Optional[Template]
extra_attributes: Dict[str, Template]
+ confirm_localpart: bool = False
class JinjaOidcMappingProvider(OidcMappingProvider[JinjaOidcMappingConfig]):
@@ -1357,12 +1359,17 @@ class JinjaOidcMappingProvider(OidcMappingProvider[JinjaOidcMappingConfig]):
"invalid jinja template", path=["extra_attributes", key]
) from e
+ confirm_localpart = config.get("confirm_localpart") or False
+ if not isinstance(confirm_localpart, bool):
+ raise ConfigError("must be a bool", path=["confirm_localpart"])
+
return JinjaOidcMappingConfig(
subject_claim=subject_claim,
localpart_template=localpart_template,
display_name_template=display_name_template,
email_template=email_template,
extra_attributes=extra_attributes,
+ confirm_localpart=confirm_localpart,
)
def get_remote_user_id(self, userinfo: UserInfo) -> str:
@@ -1398,7 +1405,10 @@ class JinjaOidcMappingProvider(OidcMappingProvider[JinjaOidcMappingConfig]):
emails.append(email)
return UserAttributeDict(
- localpart=localpart, display_name=display_name, emails=emails
+ localpart=localpart,
+ display_name=display_name,
+ emails=emails,
+ confirm_localpart=self._config.confirm_localpart,
)
async def get_extra_attributes(self, userinfo: UserInfo, token: Token) -> JsonDict:
diff --git a/synapse/handlers/sso.py b/synapse/handlers/sso.py
index ff5b5169ca..4f02a060d9 100644
--- a/synapse/handlers/sso.py
+++ b/synapse/handlers/sso.py
@@ -132,6 +132,7 @@ class UserAttributes:
# if `None`, the mapper has not picked a userid, and the user should be prompted to
# enter one.
localpart: Optional[str]
+ confirm_localpart: bool = False
display_name: Optional[str] = None
emails: Collection[str] = attr.Factory(list)
@@ -561,9 +562,10 @@ class SsoHandler:
# Must provide either attributes or session, not both
assert (attributes is not None) != (session is not None)
- if (attributes and attributes.localpart is None) or (
- session and session.chosen_localpart is None
- ):
+ if (
+ attributes
+ and (attributes.localpart is None or attributes.confirm_localpart is True)
+ ) or (session and session.chosen_localpart is None):
return b"/_synapse/client/pick_username/account_details"
elif self._consent_at_registration and not (
session and session.terms_accepted_version
diff --git a/synapse/res/templates/sso_auth_account_details.html b/synapse/res/templates/sso_auth_account_details.html
index 00e1dcdbb8..41315e4fd4 100644
--- a/synapse/res/templates/sso_auth_account_details.html
+++ b/synapse/res/templates/sso_auth_account_details.html
@@ -130,15 +130,15 @@
</head>
<body>
<header>
- <h1>Your account is nearly ready</h1>
- <p>Check your details before creating an account on {{ server_name }}</p>
+ <h1>Choose your user name</h1>
+ <p>This is required to create your account on {{ server_name }}, and you can't change this later.</p>
</header>
<main>
<form method="post" class="form__input" id="form">
<div class="username_input" id="username_input">
<label for="field-username">Username</label>
<div class="prefix">@</div>
- <input type="text" name="username" id="field-username" autofocus>
+ <input type="text" name="username" id="field-username" value="{{ user_attributes.localpart }}" autofocus>
<div class="postfix">:{{ server_name }}</div>
</div>
<output for="username_input" id="field-username-output"></output>
diff --git a/synapse/rest/synapse/client/pick_username.py b/synapse/rest/synapse/client/pick_username.py
index 28ae083497..6338fbaaa9 100644
--- a/synapse/rest/synapse/client/pick_username.py
+++ b/synapse/rest/synapse/client/pick_username.py
@@ -92,12 +92,20 @@ class AccountDetailsResource(DirectServeHtmlResource):
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,
},
}
|