5353logger = logging .getLogger (__name__ )
5454
5555
56+ SUCCESS_TEMPLATE = """
57+ <html>
58+ <head>
59+ <title>Success!</title>
60+ <meta name='viewport' content='width=device-width, initial-scale=1,
61+ user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
62+ <link rel="stylesheet" href="/_matrix/static/client/register/style.css">
63+ <script>
64+ if (window.onAuthDone) {
65+ window.onAuthDone();
66+ } else if (window.opener && window.opener.postMessage) {
67+ window.opener.postMessage("authDone", "*");
68+ }
69+ </script>
70+ </head>
71+ <body>
72+ <div>
73+ <p>Thank you</p>
74+ <p>You may now close this window and return to the application</p>
75+ </div>
76+ </body>
77+ </html>
78+ """
79+
80+
5681class AuthHandler (BaseHandler ):
5782 SESSION_EXPIRE_MS = 48 * 60 * 60 * 1000
5883
@@ -91,6 +116,7 @@ def __init__(self, hs):
91116 self .hs = hs # FIXME better possibility to access registrationHandler later?
92117 self .macaroon_gen = hs .get_macaroon_generator ()
93118 self ._password_enabled = hs .config .password_enabled
119+ self ._saml2_enabled = hs .config .saml2_enabled
94120
95121 # we keep this as a list despite the O(N^2) implication so that we can
96122 # keep PASSWORD first and avoid confusing clients which pick the first
@@ -106,17 +132,35 @@ def __init__(self, hs):
106132 if t not in login_types :
107133 login_types .append (t )
108134 self ._supported_login_types = login_types
135+ # Login types and UI Auth types have a heavy overlap, but are not
136+ # necessarily identical. Login types have SSO (and other login types)
137+ # added in the rest layer, see synapse.rest.client.v1.login.LoginRestServerlet.on_GET.
138+ ui_auth_types = login_types .copy ()
139+ if self ._saml2_enabled :
140+ ui_auth_types .append (LoginType .SSO )
141+ self ._supported_ui_auth_types = ui_auth_types
109142
110143 # Ratelimiter for failed auth during UIA. Uses same ratelimit config
111144 # as per `rc_login.failed_attempts`.
112145 self ._failed_uia_attempts_ratelimiter = Ratelimiter ()
113146
114147 self ._clock = self .hs .get_clock ()
115148
116- # Load the SSO redirect confirmation page HTML template
149+ # Load the SSO HTML templates.
150+
151+ # The following template is shown to the user during a client login via SSO,
152+ # after the SSO completes and before redirecting them back to their client.
153+ # It notifies the user they are about to give access to their matrix account
154+ # to the client.
117155 self ._sso_redirect_confirm_template = load_jinja2_templates (
118156 hs .config .sso_redirect_confirm_template_dir , ["sso_redirect_confirm.html" ],
119157 )[0 ]
158+ # The following template is shown during user interactive authentication
159+ # in the fallback auth scenario. It notifies the user that they are
160+ # authenticating for an operation to occur on their account.
161+ self ._sso_auth_confirm_template = load_jinja2_templates (
162+ hs .config .sso_redirect_confirm_template_dir , ["sso_auth_confirm.html" ],
163+ )[0 ]
120164
121165 self ._server_name = hs .config .server_name
122166
@@ -130,6 +174,7 @@ def validate_user_via_ui_auth(
130174 request : SynapseRequest ,
131175 request_body : Dict [str , Any ],
132176 clientip : str ,
177+ description : str ,
133178 ):
134179 """
135180 Checks that the user is who they claim to be, via a UI auth.
@@ -147,6 +192,9 @@ def validate_user_via_ui_auth(
147192
148193 clientip: The IP address of the client.
149194
195+ description: A human readable string to be displayed to the user that
196+ describes the operation happening on their account.
197+
150198 Returns:
151199 defer.Deferred[dict]: the parameters for this request (which may
152200 have been given only in a previous call).
@@ -175,11 +223,11 @@ def validate_user_via_ui_auth(
175223 )
176224
177225 # build a list of supported flows
178- flows = [[login_type ] for login_type in self ._supported_login_types ]
226+ flows = [[login_type ] for login_type in self ._supported_ui_auth_types ]
179227
180228 try :
181229 result , params , _ = yield self .check_auth (
182- flows , request , request_body , clientip
230+ flows , request , request_body , clientip , description
183231 )
184232 except LoginError :
185233 # Update the ratelimite to say we failed (`can_do_action` doesn't raise).
@@ -193,7 +241,7 @@ def validate_user_via_ui_auth(
193241 raise
194242
195243 # find the completed login type
196- for login_type in self ._supported_login_types :
244+ for login_type in self ._supported_ui_auth_types :
197245 if login_type not in result :
198246 continue
199247
@@ -224,6 +272,7 @@ def check_auth(
224272 request : SynapseRequest ,
225273 clientdict : Dict [str , Any ],
226274 clientip : str ,
275+ description : str ,
227276 ):
228277 """
229278 Takes a dictionary sent by the client in the login / registration
@@ -250,6 +299,9 @@ def check_auth(
250299
251300 clientip: The IP address of the client.
252301
302+ description: A human readable string to be displayed to the user that
303+ describes the operation happening on their account.
304+
253305 Returns:
254306 defer.Deferred[dict, dict, str]: a deferred tuple of
255307 (creds, params, session_id).
@@ -299,12 +351,18 @@ def check_auth(
299351 comparator = (request .uri , request .method , clientdict )
300352 if "ui_auth" not in session :
301353 session ["ui_auth" ] = comparator
354+ self ._save_session (session )
302355 elif session ["ui_auth" ] != comparator :
303356 raise SynapseError (
304357 403 ,
305358 "Requested operation has changed during the UI authentication session." ,
306359 )
307360
361+ # Add a human readable description to the session.
362+ if "description" not in session :
363+ session ["description" ] = description
364+ self ._save_session (session )
365+
308366 if not authdict :
309367 raise InteractiveAuthIncompleteError (
310368 self ._auth_dict_for_flows (flows , session )
@@ -991,6 +1049,56 @@ def _do_validate_hash():
9911049 else :
9921050 return defer .succeed (False )
9931051
1052+ def start_sso_ui_auth (self , redirect_url : str , session_id : str ) -> str :
1053+ """
1054+ Get the HTML for the SSO redirect confirmation page.
1055+
1056+ Args:
1057+ redirect_url: The URL to redirect to the SSO provider.
1058+ session_id: The user interactive authentication session ID.
1059+
1060+ Returns:
1061+ The HTML to render.
1062+ """
1063+ session = self ._get_session_info (session_id )
1064+ # Get the human readable operation of what is occurring, falling back to
1065+ # a generic message if it isn't available for some reason.
1066+ description = session .get ("description" , "modify your account" )
1067+ return self ._sso_auth_confirm_template .render (
1068+ description = description , redirect_url = redirect_url ,
1069+ )
1070+
1071+ def complete_sso_ui_auth (
1072+ self , registered_user_id : str , session_id : str , request : SynapseRequest ,
1073+ ):
1074+ """Having figured out a mxid for this user, complete the HTTP request
1075+
1076+ Args:
1077+ registered_user_id: The registered user ID to complete SSO login for.
1078+ request: The request to complete.
1079+ client_redirect_url: The URL to which to redirect the user at the end of the
1080+ process.
1081+ """
1082+ # Mark the stage of the authentication as successful.
1083+ sess = self ._get_session_info (session_id )
1084+ if "creds" not in sess :
1085+ sess ["creds" ] = {}
1086+ creds = sess ["creds" ]
1087+
1088+ # Save the user who authenticated with SSO, this will be used to ensure
1089+ # that the account be modified is also the person who logged in.
1090+ creds [LoginType .SSO ] = registered_user_id
1091+ self ._save_session (sess )
1092+
1093+ # Render the HTML and return.
1094+ html_bytes = SUCCESS_TEMPLATE .encode ("utf8" )
1095+ request .setResponseCode (200 )
1096+ request .setHeader (b"Content-Type" , b"text/html; charset=utf-8" )
1097+ request .setHeader (b"Content-Length" , b"%d" % (len (html_bytes ),))
1098+
1099+ request .write (html_bytes )
1100+ finish_request (request )
1101+
9941102 def complete_sso_login (
9951103 self ,
9961104 registered_user_id : str ,
0 commit comments