diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py
index bda279ab8b..dbe165ce1e 100644
--- a/synapse/handlers/auth.py
+++ b/synapse/handlers/auth.py
@@ -257,10 +257,6 @@ class AuthHandler(BaseHandler):
Takes a dictionary sent by the client in the login / registration
protocol and handles the User-Interactive Auth flow.
- As a side effect, this function fills in the 'creds' key on the user's
- session with a map, which maps each auth-type (str) to the relevant
- identity authenticated by that auth-type (mostly str, but for captcha, bool).
-
If no auth flows have been completed successfully, raises an
InteractiveAuthIncompleteError. To handle this, you can use
synapse.rest.client.v2_alpha._base.interactive_auth_handler as a
@@ -304,50 +300,47 @@ class AuthHandler(BaseHandler):
del clientdict["auth"]
if "session" in authdict:
sid = authdict["session"]
- session = self._get_session_info(sid)
-
- if len(clientdict) > 0:
- # This was designed to allow the client to omit the parameters
- # and just supply the session in subsequent calls so it split
- # auth between devices by just sharing the session, (eg. so you
- # could continue registration from your phone having clicked the
- # email auth link on there). It's probably too open to abuse
- # because it lets unauthenticated clients store arbitrary objects
- # on a homeserver.
- # Revisit: Assuming the REST APIs do sensible validation, the data
- # isn't arbintrary.
- session["clientdict"] = clientdict
- self._save_session(session)
- elif "clientdict" in session:
- clientdict = session["clientdict"]
-
- # Ensure that the queried operation does not vary between stages of
- # the UI authentication session. This is done by generating a stable
- # comparator based on the URI, method, and body (minus the auth dict)
- # and storing it during the initial query. Subsequent queries ensure
- # that this comparator has not changed.
- comparator = (request.uri, request.method, clientdict)
- if "ui_auth" not in session:
- session["ui_auth"] = comparator
- self._save_session(session)
- elif session["ui_auth"] != comparator:
- raise SynapseError(
- 403,
- "Requested operation has changed during the UI authentication session.",
+
+ # If there's no session ID, create a new session.
+ if not sid:
+ session = self._create_session(
+ clientdict, (request.uri, request.method, clientdict), description
)
+ session_id = session["id"]
- # Add a human readable description to the session.
- if "description" not in session:
- session["description"] = description
- self._save_session(session)
+ else:
+ session = self._get_session_info(sid)
+ session_id = sid
+
+ if not clientdict:
+ # This was designed to allow the client to omit the parameters
+ # and just supply the session in subsequent calls so it split
+ # auth between devices by just sharing the session, (eg. so you
+ # could continue registration from your phone having clicked the
+ # email auth link on there). It's probably too open to abuse
+ # because it lets unauthenticated clients store arbitrary objects
+ # on a homeserver.
+ # Revisit: Assuming the REST APIs do sensible validation, the data
+ # isn't arbitrary.
+ clientdict = session["clientdict"]
+
+ # Ensure that the queried operation does not vary between stages of
+ # the UI authentication session. This is done by generating a stable
+ # comparator based on the URI, method, and body (minus the auth dict)
+ # and storing it during the initial query. Subsequent queries ensure
+ # that this comparator has not changed.
+ comparator = (request.uri, request.method, clientdict)
+ if session["ui_auth"] != comparator:
+ raise SynapseError(
+ 403,
+ "Requested operation has changed during the UI authentication session.",
+ )
if not authdict:
raise InteractiveAuthIncompleteError(
- self._auth_dict_for_flows(flows, session)
+ self._auth_dict_for_flows(flows, session_id)
)
- if "creds" not in session:
- session["creds"] = {}
creds = session["creds"]
# check auth type currently being presented
@@ -387,9 +380,9 @@ class AuthHandler(BaseHandler):
list(clientdict),
)
- return creds, clientdict, session["id"]
+ return creds, clientdict, session_id
- ret = self._auth_dict_for_flows(flows, session)
+ ret = self._auth_dict_for_flows(flows, session_id)
ret["completed"] = list(creds)
ret.update(errordict)
raise InteractiveAuthIncompleteError(ret)
@@ -407,8 +400,6 @@ class AuthHandler(BaseHandler):
raise LoginError(400, "", Codes.MISSING_PARAM)
sess = self._get_session_info(authdict["session"])
- if "creds" not in sess:
- sess["creds"] = {}
creds = sess["creds"]
result = await self.checkers[stagetype].check_auth(authdict, clientip)
@@ -448,7 +439,7 @@ class AuthHandler(BaseHandler):
value: The data to store
"""
sess = self._get_session_info(session_id)
- sess.setdefault("serverdict", {})[key] = value
+ sess["serverdict"][key] = value
self._save_session(sess)
def get_session_data(
@@ -463,7 +454,7 @@ class AuthHandler(BaseHandler):
default: Value to return if the key has not been set
"""
sess = self._get_session_info(session_id)
- return sess.setdefault("serverdict", {}).get(key, default)
+ return sess["serverdict"].get(key, default)
async def _check_auth_dict(
self, authdict: Dict[str, Any], clientip: str
@@ -519,7 +510,7 @@ class AuthHandler(BaseHandler):
}
def _auth_dict_for_flows(
- self, flows: List[List[str]], session: Dict[str, Any]
+ self, flows: List[List[str]], session_id: str,
) -> Dict[str, Any]:
public_flows = []
for f in flows:
@@ -538,29 +529,72 @@ class AuthHandler(BaseHandler):
params[stage] = get_params[stage]()
return {
- "session": session["id"],
+ "session": session_id,
"flows": [{"stages": f} for f in public_flows],
"params": params,
}
- def _get_session_info(self, session_id: Optional[str]) -> dict:
+ def _create_session(
+ self,
+ clientdict: Dict[str, Any],
+ ui_auth: Tuple[bytes, bytes, Dict[str, Any]],
+ description: str,
+ ) -> dict:
"""
- Gets or creates a session given a session ID.
+ Creates a new user interactive authentication session.
The session can be used to track data across multiple requests, e.g. for
interactive authentication.
- """
- if session_id not in self.sessions:
- session_id = None
- if not session_id:
- # create a new session
- while session_id is None or session_id in self.sessions:
- session_id = stringutils.random_string(24)
- self.sessions[session_id] = {"id": session_id}
+ Each session has the following keys:
+
+ id:
+ A unique identifier for this session. Passed back to the client
+ and returned for each stage.
+ clientdict:
+ The dictionary from the client root level, not the 'auth' key.
+ ui_auth:
+ A tuple which is checked at each stage of the authentication to
+ ensure that the asked for operation has not changed.
+ creds:
+ A map, which maps each auth-type (str) to the relevant identity
+ authenticated by that auth-type (mostly str, but for captcha, bool).
+ serverdict:
+ A map of data that is stored server-side and cannot be modified
+ by the client.
+ description:
+ A string description of the operation that the current
+ authentication is authorising.
+ Returns:
+ The newly created session.
+ """
+ session_id = None
+ while session_id is None or session_id in self.sessions:
+ session_id = stringutils.random_string(24)
+
+ self.sessions[session_id] = {
+ "id": session_id,
+ "clientdict": clientdict,
+ "ui_auth": ui_auth,
+ "creds": {},
+ "serverdict": {},
+ "description": description,
+ }
return self.sessions[session_id]
+ def _get_session_info(self, session_id: str) -> dict:
+ """
+ Gets a session given a session ID.
+
+ The session can be used to track data across multiple requests, e.g. for
+ interactive authentication.
+ """
+ try:
+ return self.sessions[session_id]
+ except KeyError:
+ raise SynapseError(400, "Unknown session ID: %s" % (session_id,))
+
async def get_access_token_for_user_id(
self, user_id: str, device_id: Optional[str], valid_until_ms: Optional[int]
):
@@ -1030,11 +1064,8 @@ class AuthHandler(BaseHandler):
The HTML to render.
"""
session = self._get_session_info(session_id)
- # Get the human readable operation of what is occurring, falling back to
- # a generic message if it isn't available for some reason.
- description = session.get("description", "modify your account")
return self._sso_auth_confirm_template.render(
- description=description, redirect_url=redirect_url,
+ description=session["description"], redirect_url=redirect_url,
)
def complete_sso_ui_auth(
@@ -1050,8 +1081,6 @@ class AuthHandler(BaseHandler):
"""
# Mark the stage of the authentication as successful.
sess = self._get_session_info(session_id)
- if "creds" not in sess:
- sess["creds"] = {}
creds = sess["creds"]
# Save the user who authenticated with SSO, this will be used to ensure
|