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

Implement MSC3231: Token authenticated registration #10142

Merged
merged 54 commits into from
Aug 21, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
5856f81
Hard-coded token authenticated registration
govynnus Jun 3, 2021
5f21580
Create registration_tokens table
govynnus Jun 10, 2021
2b8726c
Check in database to validate registration token
govynnus Jun 11, 2021
5b1ec0b
Increment `completed` when registration token used
govynnus Jun 14, 2021
15e5769
Rename total_uses to uses_allowed
govynnus Jun 14, 2021
9c502b0
Improve unit tests
govynnus Jun 14, 2021
e7754a9
Increment pending while registration in progress
govynnus Jun 14, 2021
ef05a6d
Add unit test for registration token expiry
govynnus Jun 16, 2021
53f0e05
Fix config file related bits
govynnus Jun 16, 2021
7883191
Run connected database ops in same transaction
govynnus Jun 16, 2021
1debc22
Fix some formatting problems
govynnus Jun 17, 2021
6ac376d
Test `completed` is empty when auth should fail
govynnus Jun 17, 2021
dfa8fec
Override type of simple_select_one_txn
govynnus Jun 17, 2021
c89d786
Raise error if token changes during UIA
govynnus Jun 17, 2021
e7bd00a
Add validity checking endpoint
govynnus Jun 18, 2021
d6704fd
Use AuthHandler methods for accessing UIA session
govynnus Jun 20, 2021
003e67d
Rate limit validity checking endpoint
govynnus Jun 29, 2021
3c51680
Use LoginError rather than SynapseError in checker
govynnus Jun 29, 2021
af90be7
Add fallback
govynnus Jun 30, 2021
1552b70
Docs for currently non-existent admin API
govynnus Jul 6, 2021
4df4a6e
Implement admin API
govynnus Jul 10, 2021
6901eee
Move admin api docs to correct location
govynnus Jul 19, 2021
93f752d
Include general API shape in docstrings
govynnus Jul 20, 2021
b2bf3ac
More input validation when creating and updating
govynnus Jul 20, 2021
5d5bdef
Add space to SQL query
govynnus Jul 20, 2021
b61c7f6
Fix SQL query for invalid tokens
govynnus Jul 20, 2021
e7495e6
Decrease pending when UIA session expires
govynnus Jul 22, 2021
39d24d2
Add type to test argument
govynnus Jul 23, 2021
70cc9d2
Add test for session expiry with deleted token
govynnus Jul 23, 2021
09f6572
Use f-strings rather than str.format()
govynnus Jul 27, 2021
36adec4
Update docs/usage/administration/admin_api/registration_tokens.md
govynnus Jul 27, 2021
7e539f5
Use more descriptive name
govynnus Jul 27, 2021
1cf29c9
Return 200 when nothing to update
govynnus Jul 27, 2021
7f9efcd
Remove unneeded else and add missing f
govynnus Jul 27, 2021
e9435f8
Run linter
govynnus Jul 27, 2021
f6e4831
Add uses_allowed to updating example in docstring
govynnus Aug 12, 2021
7208760
Add return values to docstring
govynnus Aug 12, 2021
47b8837
Add docstring to validity checking endpoint
govynnus Aug 12, 2021
b76099e
Move functions into RegistrationWorkerStore
govynnus Aug 12, 2021
86bbc24
Merge branch 'develop' into token-registration
govynnus Aug 19, 2021
c6cb80b
Add link to admin API docs in config file
govynnus Aug 19, 2021
ba22ffd
Move table creation SQL to latest delta
govynnus Aug 19, 2021
c775dce
Add changelog entry
govynnus Aug 19, 2021
f327b29
Regenerate sample config
govynnus Aug 19, 2021
c6bcae2
Move table creation sql to actual newest delta
govynnus Aug 20, 2021
01a74da
Avoid integrity error when creating tokens
govynnus Aug 20, 2021
5bfc707
Fix docs, comments and variable names
govynnus Aug 20, 2021
b5608c3
Try again if generated token already exists
govynnus Aug 20, 2021
bf28876
Let validity checking endpoint be used by workers
govynnus Aug 20, 2021
2e59dda
Document usage of `null` when updating tokens
govynnus Aug 20, 2021
df0077d
Merge remote-tracking branch 'upstream/develop' into token-registration
govynnus Aug 20, 2021
54867ef
Simplify retrying of token generation
govynnus Aug 21, 2021
20b566c
Small additions to admin api documentation
govynnus Aug 21, 2021
04b237a
Update synapse/storage/databases/main/registration.py
anoadragon453 Aug 21, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions synapse/config/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,30 @@ def read_config(self, config, **kwargs):
session_lifetime = self.parse_duration(session_lifetime)
self.session_lifetime = session_lifetime

# The `access_token_lifetime` applies for tokens that can be renewed
# using a refresh token, as per MSC2918. If it is `None`, the refresh
# token mechanism is disabled.
#
# Since it is incompatible with the `session_lifetime` mechanism, it is set to
# `None` by default if a `session_lifetime` is set.
access_token_lifetime = config.get(
"access_token_lifetime", "5m" if session_lifetime is None else None
)
if access_token_lifetime is not None:
access_token_lifetime = self.parse_duration(access_token_lifetime)
self.access_token_lifetime = access_token_lifetime

if session_lifetime is not None and access_token_lifetime is not None:
raise ConfigError(
"The refresh token mechanism is incompatible with the "
"`session_lifetime` option. Consider disabling the "
"`session_lifetime` option or disabling the refresh token "
"mechanism by removing the `access_token_lifetime` option."
)

# The fallback template used for authenticating using a registration token
self.registration_token_template = self.read_template("registration_token.html")

# The success template used during fallback auth.
self.fallback_success_template = self.read_template("auth_success.html")

Expand Down
3 changes: 3 additions & 0 deletions synapse/res/templates/registration_token.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
<body>
<form id="registrationForm" method="post" action="{{ myurl }}">
<div>
{% if error is defined %}
<p class="error"><strong>Error: {{ error }}</strong></p>
{% endif %}
<p>
Please enter a registration token.
</p>
Expand Down
2 changes: 1 addition & 1 deletion synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
)
from synapse.rest.admin.groups import DeleteGroupAdminRestServlet
from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo
from synapse.rest.admin.purge_room_servlet import PurgeRoomServlet
from synapse.rest.admin.registration_tokens import (
ListRegistrationTokensRestServlet,
NewRegistrationTokenRestServlet,
Expand Down Expand Up @@ -243,6 +242,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
ForwardExtremitiesRestServlet(hs).register(http_server)
RoomEventContextServlet(hs).register(http_server)
RateLimitRestServlet(hs).register(http_server)
UsernameAvailableRestServlet(hs).register(http_server)
ListRegistrationTokensRestServlet(hs).register(http_server)
NewRegistrationTokenRestServlet(hs).register(http_server)
RegistrationTokenRestServlet(hs).register(http_server)
Expand Down
9 changes: 1 addition & 8 deletions synapse/rest/admin/registration_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
400, "expiry_time must not be in the past", Codes.INVALID_PARAM
)

res = await self.store.create_registration_token(
token, uses_allowed, expiry_time
)
if not res:
# Creation failed, probably the token already exists
raise SynapseError(
400, f"Token already exists: {token}", Codes.INVALID_PARAM
)
await self.store.create_registration_token(token, uses_allowed, expiry_time)

resp = {
"token": token,
Expand Down
23 changes: 15 additions & 8 deletions synapse/rest/client/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ async def on_POST(self, request, stagetype):
sitekey=self.hs.config.recaptcha_public_key,
error=e.msg,
)
else:
# No LoginError was raised, so authentication was successful
html = self.success_template.render()

elif stagetype == LoginType.TERMS:
authdict = {"session": session}
Expand All @@ -136,26 +139,30 @@ async def on_POST(self, request, stagetype):
% (CLIENT_API_PREFIX, LoginType.TERMS),
error=e.msg,
)
else:
# No LoginError was raised, so authentication was successful
html = self.success_template.render()

elif stagetype == LoginType.SSO:
# The SSO fallback workflow should not post here,
raise SynapseError(404, "Fallback SSO auth does not support POST requests.")

elif stagetype == LoginType.REGISTRATION_TOKEN:
token = parse_string(request, "token")
token = parse_string(request, "token", required=True)
authdict = {"session": session, "token": token}

success = await self.auth_handler.add_oob_auth(
LoginType.REGISTRATION_TOKEN, authdict, request.getClientIP()
)

if success:
html = self.success_template.render()
else:
try:
await self.auth_handler.add_oob_auth(
LoginType.REGISTRATION_TOKEN, authdict, request.getClientIP()
)
except LoginError as e:
html = self.registration_token_template.render(
session=session,
myurl=f"{CLIENT_API_PREFIX}/r0/auth/{LoginType.REGISTRATION_TOKEN}/fallback/web",
error=e.msg,
)
else:
html = self.success_template.render()

else:
raise SynapseError(404, "Unknown auth stage type")
Expand Down
135 changes: 122 additions & 13 deletions synapse/storage/databases/main/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1328,7 +1328,7 @@ async def get_one_registration_token(self, token: str) -> Optional[Dict[str, Any

async def create_registration_token(
self, token: str, uses_allowed: Optional[int], expiry_time: Optional[int]
) -> bool:
) -> None:
"""Create a new registration token. Used by the admin API.

Args:
Expand All @@ -1343,18 +1343,22 @@ async def create_registration_token(
Returns:
Whether the row was inserted or not.
"""
return await self.db_pool.simple_insert(
"registration_tokens",
values={
"token": token,
"uses_allowed": uses_allowed,
"pending": 0,
"completed": 0,
"expiry_time": expiry_time,
},
or_ignore=True,
desc="create_registration_token",
)
try:
await self.db_pool.simple_insert(
"registration_tokens",
values={
"token": token,
"uses_allowed": uses_allowed,
"pending": 0,
"completed": 0,
"expiry_time": expiry_time,
},
desc="create_registration_token",
)
except self.database_engine.module.IntegrityError:
govynnus marked this conversation as resolved.
Show resolved Hide resolved
raise SynapseError(
400, f"Token already exists: {token}", Codes.INVALID_PARAM
)

async def update_registration_token(
self, token: str, updatevalues: Dict[str, Optional[int]]
Expand Down Expand Up @@ -1424,6 +1428,111 @@ async def delete_registration_token(self, token: str) -> bool:

return True

@cached()
async def mark_access_token_as_used(self, token_id: int) -> None:
"""
Mark the access token as used, which invalidates the refresh token used
to obtain it.

Because get_user_by_access_token is cached, this function might be
called multiple times for the same token, effectively doing unnecessary
SQL updates. Because updating the `used` field only goes one way (from
False to True) it is safe to cache this function as well to avoid this
issue.

Args:
token_id: The ID of the access token to update.
Raises:
StoreError if there was a problem updating this.
"""
await self.db_pool.simple_update_one(
"access_tokens",
{"id": token_id},
{"used": True},
desc="mark_access_token_as_used",
)

async def lookup_refresh_token(
self, token: str
) -> Optional[RefreshTokenLookupResult]:
"""Lookup a refresh token with hints about its validity."""

def _lookup_refresh_token_txn(txn) -> Optional[RefreshTokenLookupResult]:
txn.execute(
"""
SELECT
rt.id token_id,
rt.user_id,
rt.device_id,
rt.next_token_id,
(nrt.next_token_id IS NOT NULL) has_next_refresh_token_been_refreshed,
at.used has_next_access_token_been_used
FROM refresh_tokens rt
LEFT JOIN refresh_tokens nrt ON rt.next_token_id = nrt.id
LEFT JOIN access_tokens at ON at.refresh_token_id = nrt.id
WHERE rt.token = ?
""",
(token,),
)
row = txn.fetchone()

if row is None:
return None

return RefreshTokenLookupResult(
token_id=row[0],
user_id=row[1],
device_id=row[2],
next_token_id=row[3],
has_next_refresh_token_been_refreshed=row[4],
# This column is nullable, ensure it's a boolean
has_next_access_token_been_used=(row[5] or False),
)

return await self.db_pool.runInteraction(
"lookup_refresh_token", _lookup_refresh_token_txn
)

async def replace_refresh_token(self, token_id: int, next_token_id: int) -> None:
"""
Set the successor of a refresh token, removing the existing successor
if any.

Args:
token_id: ID of the refresh token to update.
next_token_id: ID of its successor.
"""

def _replace_refresh_token_txn(txn) -> None:
# First check if there was an existing refresh token
old_next_token_id = self.db_pool.simple_select_one_onecol_txn(
txn,
"refresh_tokens",
{"id": token_id},
"next_token_id",
allow_none=True,
)

self.db_pool.simple_update_one_txn(
txn,
"refresh_tokens",
{"id": token_id},
{"next_token_id": next_token_id},
)

# Delete the old "next" token if it exists. This should cascade and
# delete the associated access_token
if old_next_token_id is not None:
self.db_pool.simple_delete_one_txn(
txn,
"refresh_tokens",
{"id": old_next_token_id},
)

await self.db_pool.runInteraction(
"replace_refresh_token", _replace_refresh_token_txn
)


class RegistrationBackgroundUpdateStore(RegistrationWorkerStore):
def __init__(
Expand Down
2 changes: 1 addition & 1 deletion tests/rest/admin/test_registration_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import synapse.rest.admin
from synapse.api.errors import Codes
from synapse.rest.client.v1 import login
from synapse.rest.client import login

from tests import unittest

Expand Down
3 changes: 1 addition & 2 deletions tests/rest/client/v2_alpha/test_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
from synapse.api.constants import APP_SERVICE_REGISTRATION_TYPE, LoginType
from synapse.api.errors import Codes
from synapse.appservice import ApplicationService
from synapse.rest.client.v1 import login, logout
from synapse.rest.client.v2_alpha import account, account_validity, register, sync
from synapse.rest.client import account, account_validity, login, logout, register, sync
from synapse.storage._base import db_to_json

from tests import unittest
Expand Down
You are viewing a condensed version of this merge commit. You can view the full changes here.