Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Persist the storage of UI Auth sessions into the database.
Browse files Browse the repository at this point in the history
  • Loading branch information
clokep committed Apr 17, 2020
1 parent 6fa823c commit bb311c0
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 111 deletions.
123 changes: 16 additions & 107 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
from synapse.module_api import ModuleApi
from synapse.push.mailer import load_jinja2_templates
from synapse.types import Requester, UserID
from synapse.util.caches.expiringcache import ExpiringCache

from ._base import BaseHandler

Expand Down Expand Up @@ -77,8 +76,6 @@


class AuthHandler(BaseHandler):
SESSION_EXPIRE_MS = 48 * 60 * 60 * 1000

def __init__(self, hs):
"""
Args:
Expand All @@ -94,15 +91,6 @@ def __init__(self, hs):

self.bcrypt_rounds = hs.config.bcrypt_rounds

# This is not a cache per se, but a store of all current sessions that
# expire after N hours
self.sessions = ExpiringCache(
cache_name="register_sessions",
clock=hs.get_clock(),
expiry_ms=self.SESSION_EXPIRE_MS,
reset_expiry_on_get=True,
)

account_handler = ModuleApi(hs, self)
self.password_providers = [
module(config=config, account_handler=account_handler)
Expand Down Expand Up @@ -323,13 +311,12 @@ async def check_auth(

# 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 = await self.store.create_session(
clientdict, request.uri, request.method, description
)
session_id = session["id"]

else:
session = self._get_session_info(sid)
session = await self.store.get_session(sid)
session_id = sid

if not clientdict:
Expand All @@ -350,7 +337,7 @@ async def check_auth(
# 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:
if (session["uri"], session["method"], session["clientdict"]) != comparator:
raise SynapseError(
403,
"Requested operation has changed during the UI authentication session.",
Expand All @@ -361,17 +348,14 @@ async def check_auth(
self._auth_dict_for_flows(flows, session_id)
)

creds = session["creds"]

# check auth type currently being presented
errordict = {} # type: Dict[str, Any]
if "type" in authdict:
login_type = authdict["type"] # type: str
try:
result = await self._check_auth_dict(authdict, clientip)
if result:
creds[login_type] = result
self._save_session(session)
await self.store.mark_stage_complete(session_id, login_type, result)
except LoginError as e:
if login_type == LoginType.EMAIL_IDENTITY:
# riot used to have a bug where it would request a new
Expand All @@ -387,6 +371,7 @@ async def check_auth(
# so that the client can have another go.
errordict = e.error_dict()

creds = await self.store.get_completed_stages(session_id)
for f in flows:
if len(set(f) - set(creds)) == 0:
# it's very useful to know what args are stored, but this can
Expand Down Expand Up @@ -419,13 +404,9 @@ async def add_oob_auth(
if "session" not in authdict:
raise LoginError(400, "", Codes.MISSING_PARAM)

sess = self._get_session_info(authdict["session"])
creds = sess["creds"]

result = await self.checkers[stagetype].check_auth(authdict, clientip)
if result:
creds[stagetype] = result
self._save_session(sess)
await self.store.mark_stage_complete(authdict["session"], stagetype, result)
return True
return False

Expand All @@ -447,7 +428,7 @@ def get_session_id(self, clientdict: Dict[str, Any]) -> Optional[str]:
sid = authdict["session"]
return sid

def set_session_data(self, session_id: str, key: str, value: Any) -> None:
async def set_session_data(self, session_id: str, key: str, value: Any) -> None:
"""
Store a key-value pair into the sessions data associated with this
request. This data is stored server-side and cannot be modified by
Expand All @@ -458,11 +439,9 @@ def set_session_data(self, session_id: str, key: str, value: Any) -> None:
key: The key to store the data under
value: The data to store
"""
sess = self._get_session_info(session_id)
sess["serverdict"][key] = value
self._save_session(sess)
await self.store.set_session_data(session_id, key, value)

def get_session_data(
async def get_session_data(
self, session_id: str, key: str, default: Optional[Any] = None
) -> Any:
"""
Expand All @@ -473,8 +452,7 @@ def get_session_data(
key: The key to store the data under
default: Value to return if the key has not been set
"""
sess = self._get_session_info(session_id)
return sess["serverdict"].get(key, default)
return await self.store.get_session_data(session_id, key, default)

async def _check_auth_dict(
self, authdict: Dict[str, Any], clientip: str
Expand Down Expand Up @@ -554,66 +532,6 @@ def _auth_dict_for_flows(
"params": params,
}

def _create_session(
self,
clientdict: Dict[str, Any],
ui_auth: Tuple[str, str, Dict[str, Any]],
description: str,
) -> dict:
"""
Creates a new user interactive authentication session.
The session can be used to track data across multiple requests, e.g. for
interactive authentication.
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.
"""
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]
):
Expand Down Expand Up @@ -1013,13 +931,6 @@ async def delete_threepid(
await self.store.user_delete_threepid(user_id, medium, address)
return result

def _save_session(self, session: Dict[str, Any]) -> None:
"""Update the last used time on the session to now and add it back to the session store."""
# TODO: Persistent storage
logger.debug("Saving session %s", session)
session["last_used"] = self.hs.get_clock().time_msec()
self.sessions[session["id"]] = session

async def hash(self, password: str) -> str:
"""Computes a secure hash of password.
Expand Down Expand Up @@ -1082,12 +993,12 @@ def start_sso_ui_auth(self, redirect_url: str, session_id: str) -> str:
Returns:
The HTML to render.
"""
session = self._get_session_info(session_id)
session = self.store.get_session(session_id)
return self._sso_auth_confirm_template.render(
description=session["description"], redirect_url=redirect_url,
)

def complete_sso_ui_auth(
async def complete_sso_ui_auth(
self, registered_user_id: str, session_id: str, request: SynapseRequest,
):
"""Having figured out a mxid for this user, complete the HTTP request
Expand All @@ -1099,13 +1010,11 @@ def complete_sso_ui_auth(
process.
"""
# Mark the stage of the authentication as successful.
sess = self._get_session_info(session_id)
creds = sess["creds"]

# Save the user who authenticated with SSO, this will be used to ensure
# that the account be modified is also the person who logged in.
creds[LoginType.SSO] = registered_user_id
self._save_session(sess)
await self.store.mark_stage_complete(
session_id, LoginType.SSO, registered_user_id
)

# Render the HTML and return.
html_bytes = SUCCESS_TEMPLATE.encode("utf8")
Expand Down
2 changes: 1 addition & 1 deletion synapse/handlers/cas_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ async def handle_ticket(
registered_user_id = await self._auth_handler.check_user_exists(user_id)

if session:
self._auth_handler.complete_sso_ui_auth(
await self._auth_handler.complete_sso_ui_auth(
registered_user_id, session, request,
)

Expand Down
2 changes: 1 addition & 1 deletion synapse/handlers/saml_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ async def handle_saml_response(self, request):

# Complete the interactive auth session or the login.
if current_session and current_session.ui_auth_session_id:
self._auth_handler.complete_sso_ui_auth(
await self._auth_handler.complete_sso_ui_auth(
user_id, current_session.ui_auth_session_id, request
)

Expand Down
4 changes: 2 additions & 2 deletions synapse/rest/client/v2_alpha/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ async def on_POST(self, request):
# registered a user for this session, so we could just return the
# user here. We carry on and go through the auth checks though,
# for paranoia.
registered_user_id = self.auth_handler.get_session_data(
registered_user_id = await self.auth_handler.get_session_data(
session_id, "registered_user_id", None
)

Expand Down Expand Up @@ -588,7 +588,7 @@ async def on_POST(self, request):

# remember that we've now registered that user account, and with
# what user ID (since the user may not have specified)
self.auth_handler.set_session_data(
await self.auth_handler.set_session_data(
session_id, "registered_user_id", registered_user_id
)

Expand Down
2 changes: 2 additions & 0 deletions synapse/storage/data_stores/main/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
from .stream import StreamStore
from .tags import TagsStore
from .transactions import TransactionStore
from .ui_auth import UIAuthStore
from .user_directory import UserDirectoryStore
from .user_erasure_store import UserErasureStore

Expand Down Expand Up @@ -112,6 +113,7 @@ class DataStore(
StatsStore,
RelationsStore,
CacheInvalidationStore,
UIAuthStore,
):
def __init__(self, database: Database, db_conn, hs):
self.hs = hs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* Copyright 2020 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.
*/

CREATE TABLE IF NOT EXISTS ui_auth_sessions(
session_id TEXT NOT NULL, -- The session ID passed to the client.
last_used BIGINT UNSIGNED NOT NULL, -- The last time this session was seen.
serverdict TEXT NOT NULL, -- A JSON dictionary of arbitrary data added by Synapse.
clientdict TEXT NOT NULL, -- A JSON dictionary of arbitrary data from the client.
uri TEXT NOT NULL, -- The URI the UI authentication session is using.
method TEXT NOT NULL, -- The HTTP method the UI authentication session is using.
-- The clientdict, uri, and method make up an tuple that must be immutable
-- throughout the lifetime of the UI Auth session.
description TEXT NOT NULL, -- A human readable description of the operation which caused the UI Auth flow to occur.
UNIQUE (session_id)
);

CREATE TABLE IF NOT EXISTS ui_auth_sessions_credentials(
session_id TEXT NOT NULL, -- The corresponding UI Auth session.
stage_type TEXT NOT NULL, -- The stage type.
identity TEXT NOT NULL, -- The identity authenticated by the stage, stored as JSON.
UNIQUE (session_id, stage_type)
);
Loading

0 comments on commit bb311c0

Please sign in to comment.