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

Commit 7eb6e39

Browse files
authored
Record the SSO Auth Provider in the login token (#9510)
This great big stack of commits is a a whole load of hoop-jumping to make it easier to store additional values in login tokens, and then to actually store the SSO Identity Provider in the login token. (Making use of that data will follow in a subsequent PR.)
1 parent a6333b8 commit 7eb6e39

File tree

13 files changed

+258
-151
lines changed

13 files changed

+258
-151
lines changed

changelog.d/9510.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add prometheus metrics for number of users successfully registering and logging in.

mypy.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ files =
6969
synapse/util/async_helpers.py,
7070
synapse/util/caches,
7171
synapse/util/metrics.py,
72+
synapse/util/macaroons.py,
7273
synapse/util/stringutils.py,
7374
tests/replication,
7475
tests/test_utils,

synapse/api/auth.py

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from synapse.storage.databases.main.registration import TokenLookupResult
4040
from synapse.types import StateMap, UserID
4141
from synapse.util.caches.lrucache import LruCache
42+
from synapse.util.macaroons import get_value_from_macaroon, satisfy_expiry
4243
from synapse.util.metrics import Measure
4344

4445
logger = logging.getLogger(__name__)
@@ -408,43 +409,27 @@ def _parse_and_validate_macaroon(self, token, rights="access"):
408409
raise _InvalidMacaroonException()
409410

410411
try:
411-
user_id = self.get_user_id_from_macaroon(macaroon)
412+
user_id = get_value_from_macaroon(macaroon, "user_id")
412413

413414
guest = False
414415
for caveat in macaroon.caveats:
415416
if caveat.caveat_id == "guest = true":
416417
guest = True
417418

418419
self.validate_macaroon(macaroon, rights, user_id=user_id)
419-
except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError):
420+
except (
421+
pymacaroons.exceptions.MacaroonException,
422+
KeyError,
423+
TypeError,
424+
ValueError,
425+
):
420426
raise InvalidClientTokenError("Invalid macaroon passed.")
421427

422428
if rights == "access":
423429
self.token_cache[token] = (user_id, guest)
424430

425431
return user_id, guest
426432

427-
def get_user_id_from_macaroon(self, macaroon):
428-
"""Retrieve the user_id given by the caveats on the macaroon.
429-
430-
Does *not* validate the macaroon.
431-
432-
Args:
433-
macaroon (pymacaroons.Macaroon): The macaroon to validate
434-
435-
Returns:
436-
(str) user id
437-
438-
Raises:
439-
InvalidClientCredentialsError if there is no user_id caveat in the
440-
macaroon
441-
"""
442-
user_prefix = "user_id = "
443-
for caveat in macaroon.caveats:
444-
if caveat.caveat_id.startswith(user_prefix):
445-
return caveat.caveat_id[len(user_prefix) :]
446-
raise InvalidClientTokenError("No user caveat in macaroon")
447-
448433
def validate_macaroon(self, macaroon, type_string, user_id):
449434
"""
450435
validate that a Macaroon is understood by and was signed by this server.
@@ -465,21 +450,13 @@ def validate_macaroon(self, macaroon, type_string, user_id):
465450
v.satisfy_exact("type = " + type_string)
466451
v.satisfy_exact("user_id = %s" % user_id)
467452
v.satisfy_exact("guest = true")
468-
v.satisfy_general(self._verify_expiry)
453+
satisfy_expiry(v, self.clock.time_msec)
469454

470455
# access_tokens include a nonce for uniqueness: any value is acceptable
471456
v.satisfy_general(lambda c: c.startswith("nonce = "))
472457

473458
v.verify(macaroon, self._macaroon_secret_key)
474459

475-
def _verify_expiry(self, caveat):
476-
prefix = "time < "
477-
if not caveat.startswith(prefix):
478-
return False
479-
expiry = int(caveat[len(prefix) :])
480-
now = self.hs.get_clock().time_msec()
481-
return now < expiry
482-
483460
def get_appservice_by_req(self, request: SynapseRequest) -> ApplicationService:
484461
token = self.get_access_token_from_request(request)
485462
service = self.store.get_app_service_by_token(token)

synapse/handlers/auth.py

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
from synapse.types import JsonDict, Requester, UserID
6666
from synapse.util import stringutils as stringutils
6767
from synapse.util.async_helpers import maybe_awaitable
68+
from synapse.util.macaroons import get_value_from_macaroon, satisfy_expiry
6869
from synapse.util.msisdn import phone_number_to_msisdn
6970
from synapse.util.threepids import canonicalise_email
7071

@@ -170,6 +171,16 @@ class SsoLoginExtraAttributes:
170171
extra_attributes = attr.ib(type=JsonDict)
171172

172173

174+
@attr.s(slots=True, frozen=True)
175+
class LoginTokenAttributes:
176+
"""Data we store in a short-term login token"""
177+
178+
user_id = attr.ib(type=str)
179+
180+
# the SSO Identity Provider that the user authenticated with, to get this token
181+
auth_provider_id = attr.ib(type=str)
182+
183+
173184
class AuthHandler(BaseHandler):
174185
SESSION_EXPIRE_MS = 48 * 60 * 60 * 1000
175186

@@ -1164,18 +1175,16 @@ async def _check_local_password(self, user_id: str, password: str) -> Optional[s
11641175
return None
11651176
return user_id
11661177

1167-
async def validate_short_term_login_token_and_get_user_id(self, login_token: str):
1168-
auth_api = self.hs.get_auth()
1169-
user_id = None
1178+
async def validate_short_term_login_token(
1179+
self, login_token: str
1180+
) -> LoginTokenAttributes:
11701181
try:
1171-
macaroon = pymacaroons.Macaroon.deserialize(login_token)
1172-
user_id = auth_api.get_user_id_from_macaroon(macaroon)
1173-
auth_api.validate_macaroon(macaroon, "login", user_id)
1182+
res = self.macaroon_gen.verify_short_term_login_token(login_token)
11741183
except Exception:
11751184
raise AuthError(403, "Invalid token", errcode=Codes.FORBIDDEN)
11761185

1177-
await self.auth.check_auth_blocking(user_id)
1178-
return user_id
1186+
await self.auth.check_auth_blocking(res.user_id)
1187+
return res
11791188

11801189
async def delete_access_token(self, access_token: str):
11811190
"""Invalidate a single access token
@@ -1397,6 +1406,7 @@ async def start_sso_ui_auth(self, request: SynapseRequest, session_id: str) -> s
13971406
async def complete_sso_login(
13981407
self,
13991408
registered_user_id: str,
1409+
auth_provider_id: str,
14001410
request: Request,
14011411
client_redirect_url: str,
14021412
extra_attributes: Optional[JsonDict] = None,
@@ -1406,6 +1416,9 @@ async def complete_sso_login(
14061416
14071417
Args:
14081418
registered_user_id: The registered user ID to complete SSO login for.
1419+
auth_provider_id: The id of the SSO Identity provider that was used for
1420+
login. This will be stored in the login token for future tracking in
1421+
prometheus metrics.
14091422
request: The request to complete.
14101423
client_redirect_url: The URL to which to redirect the user at the end of the
14111424
process.
@@ -1427,6 +1440,7 @@ async def complete_sso_login(
14271440

14281441
self._complete_sso_login(
14291442
registered_user_id,
1443+
auth_provider_id,
14301444
request,
14311445
client_redirect_url,
14321446
extra_attributes,
@@ -1437,6 +1451,7 @@ async def complete_sso_login(
14371451
def _complete_sso_login(
14381452
self,
14391453
registered_user_id: str,
1454+
auth_provider_id: str,
14401455
request: Request,
14411456
client_redirect_url: str,
14421457
extra_attributes: Optional[JsonDict] = None,
@@ -1463,7 +1478,7 @@ def _complete_sso_login(
14631478

14641479
# Create a login token
14651480
login_token = self.macaroon_gen.generate_short_term_login_token(
1466-
registered_user_id
1481+
registered_user_id, auth_provider_id=auth_provider_id
14671482
)
14681483

14691484
# Append the login token to the original redirect URL (i.e. with its query
@@ -1569,15 +1584,48 @@ def generate_access_token(
15691584
return macaroon.serialize()
15701585

15711586
def generate_short_term_login_token(
1572-
self, user_id: str, duration_in_ms: int = (2 * 60 * 1000)
1587+
self,
1588+
user_id: str,
1589+
auth_provider_id: str,
1590+
duration_in_ms: int = (2 * 60 * 1000),
15731591
) -> str:
15741592
macaroon = self._generate_base_macaroon(user_id)
15751593
macaroon.add_first_party_caveat("type = login")
15761594
now = self.hs.get_clock().time_msec()
15771595
expiry = now + duration_in_ms
15781596
macaroon.add_first_party_caveat("time < %d" % (expiry,))
1597+
macaroon.add_first_party_caveat("auth_provider_id = %s" % (auth_provider_id,))
15791598
return macaroon.serialize()
15801599

1600+
def verify_short_term_login_token(self, token: str) -> LoginTokenAttributes:
1601+
"""Verify a short-term-login macaroon
1602+
1603+
Checks that the given token is a valid, unexpired short-term-login token
1604+
minted by this server.
1605+
1606+
Args:
1607+
token: the login token to verify
1608+
1609+
Returns:
1610+
the user_id that this token is valid for
1611+
1612+
Raises:
1613+
MacaroonVerificationFailedException if the verification failed
1614+
"""
1615+
macaroon = pymacaroons.Macaroon.deserialize(token)
1616+
user_id = get_value_from_macaroon(macaroon, "user_id")
1617+
auth_provider_id = get_value_from_macaroon(macaroon, "auth_provider_id")
1618+
1619+
v = pymacaroons.Verifier()
1620+
v.satisfy_exact("gen = 1")
1621+
v.satisfy_exact("type = login")
1622+
v.satisfy_general(lambda c: c.startswith("user_id = "))
1623+
v.satisfy_general(lambda c: c.startswith("auth_provider_id = "))
1624+
satisfy_expiry(v, self.hs.get_clock().time_msec)
1625+
v.verify(macaroon, self.hs.config.key.macaroon_secret_key)
1626+
1627+
return LoginTokenAttributes(user_id=user_id, auth_provider_id=auth_provider_id)
1628+
15811629
def generate_delete_pusher_token(self, user_id: str) -> str:
15821630
macaroon = self._generate_base_macaroon(user_id)
15831631
macaroon.add_first_party_caveat("type = delete_pusher")

synapse/handlers/oidc_handler.py

Lines changed: 14 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from synapse.types import JsonDict, UserID, map_username_to_mxid_localpart
4343
from synapse.util import json_decoder
4444
from synapse.util.caches.cached_call import RetryOnExceptionCachedCall
45+
from synapse.util.macaroons import get_value_from_macaroon, satisfy_expiry
4546

4647
if TYPE_CHECKING:
4748
from synapse.server import HomeServer
@@ -211,7 +212,7 @@ async def handle_oidc_callback(self, request: SynapseRequest) -> None:
211212
session_data = self._token_generator.verify_oidc_session_token(
212213
session, state
213214
)
214-
except (MacaroonDeserializationException, ValueError) as e:
215+
except (MacaroonDeserializationException, KeyError) as e:
215216
logger.exception("Invalid session for OIDC callback")
216217
self._sso_handler.render_error(request, "invalid_session", str(e))
217218
return
@@ -745,7 +746,7 @@ async def handle_redirect_request(
745746
idp_id=self.idp_id,
746747
nonce=nonce,
747748
client_redirect_url=client_redirect_url.decode(),
748-
ui_auth_session_id=ui_auth_session_id,
749+
ui_auth_session_id=ui_auth_session_id or "",
749750
),
750751
)
751752

@@ -1020,10 +1021,9 @@ def generate_oidc_session_token(
10201021
macaroon.add_first_party_caveat(
10211022
"client_redirect_url = %s" % (session_data.client_redirect_url,)
10221023
)
1023-
if session_data.ui_auth_session_id:
1024-
macaroon.add_first_party_caveat(
1025-
"ui_auth_session_id = %s" % (session_data.ui_auth_session_id,)
1026-
)
1024+
macaroon.add_first_party_caveat(
1025+
"ui_auth_session_id = %s" % (session_data.ui_auth_session_id,)
1026+
)
10271027
now = self._clock.time_msec()
10281028
expiry = now + duration_in_ms
10291029
macaroon.add_first_party_caveat("time < %d" % (expiry,))
@@ -1046,7 +1046,7 @@ def verify_oidc_session_token(
10461046
The data extracted from the session cookie
10471047
10481048
Raises:
1049-
ValueError if an expected caveat is missing from the macaroon.
1049+
KeyError if an expected caveat is missing from the macaroon.
10501050
"""
10511051
macaroon = pymacaroons.Macaroon.deserialize(session)
10521052

@@ -1057,60 +1057,23 @@ def verify_oidc_session_token(
10571057
v.satisfy_general(lambda c: c.startswith("nonce = "))
10581058
v.satisfy_general(lambda c: c.startswith("idp_id = "))
10591059
v.satisfy_general(lambda c: c.startswith("client_redirect_url = "))
1060-
# Sometimes there's a UI auth session ID, it seems to be OK to attempt
1061-
# to always satisfy this.
10621060
v.satisfy_general(lambda c: c.startswith("ui_auth_session_id = "))
1063-
v.satisfy_general(self._verify_expiry)
1061+
satisfy_expiry(v, self._clock.time_msec)
10641062

10651063
v.verify(macaroon, self._macaroon_secret_key)
10661064

10671065
# Extract the session data from the token.
1068-
nonce = self._get_value_from_macaroon(macaroon, "nonce")
1069-
idp_id = self._get_value_from_macaroon(macaroon, "idp_id")
1070-
client_redirect_url = self._get_value_from_macaroon(
1071-
macaroon, "client_redirect_url"
1072-
)
1073-
try:
1074-
ui_auth_session_id = self._get_value_from_macaroon(
1075-
macaroon, "ui_auth_session_id"
1076-
) # type: Optional[str]
1077-
except ValueError:
1078-
ui_auth_session_id = None
1079-
1066+
nonce = get_value_from_macaroon(macaroon, "nonce")
1067+
idp_id = get_value_from_macaroon(macaroon, "idp_id")
1068+
client_redirect_url = get_value_from_macaroon(macaroon, "client_redirect_url")
1069+
ui_auth_session_id = get_value_from_macaroon(macaroon, "ui_auth_session_id")
10801070
return OidcSessionData(
10811071
nonce=nonce,
10821072
idp_id=idp_id,
10831073
client_redirect_url=client_redirect_url,
10841074
ui_auth_session_id=ui_auth_session_id,
10851075
)
10861076

1087-
def _get_value_from_macaroon(self, macaroon: pymacaroons.Macaroon, key: str) -> str:
1088-
"""Extracts a caveat value from a macaroon token.
1089-
1090-
Args:
1091-
macaroon: the token
1092-
key: the key of the caveat to extract
1093-
1094-
Returns:
1095-
The extracted value
1096-
1097-
Raises:
1098-
ValueError: if the caveat was not in the macaroon
1099-
"""
1100-
prefix = key + " = "
1101-
for caveat in macaroon.caveats:
1102-
if caveat.caveat_id.startswith(prefix):
1103-
return caveat.caveat_id[len(prefix) :]
1104-
raise ValueError("No %s caveat in macaroon" % (key,))
1105-
1106-
def _verify_expiry(self, caveat: str) -> bool:
1107-
prefix = "time < "
1108-
if not caveat.startswith(prefix):
1109-
return False
1110-
expiry = int(caveat[len(prefix) :])
1111-
now = self._clock.time_msec()
1112-
return now < expiry
1113-
11141077

11151078
@attr.s(frozen=True, slots=True)
11161079
class OidcSessionData:
@@ -1125,8 +1088,8 @@ class OidcSessionData:
11251088
# The URL the client gave when it initiated the flow. ("" if this is a UI Auth)
11261089
client_redirect_url = attr.ib(type=str)
11271090

1128-
# The session ID of the ongoing UI Auth (None if this is a login)
1129-
ui_auth_session_id = attr.ib(type=Optional[str], default=None)
1091+
# The session ID of the ongoing UI Auth ("" if this is a login)
1092+
ui_auth_session_id = attr.ib(type=str)
11301093

11311094

11321095
UserAttributeDict = TypedDict(

synapse/handlers/sso.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ async def complete_sso_login_request(
456456

457457
await self._auth_handler.complete_sso_login(
458458
user_id,
459+
auth_provider_id,
459460
request,
460461
client_redirect_url,
461462
extra_login_attributes,
@@ -886,6 +887,7 @@ async def register_sso_user(self, request: Request, session_id: str) -> None:
886887

887888
await self._auth_handler.complete_sso_login(
888889
user_id,
890+
session.auth_provider_id,
889891
request,
890892
session.client_redirect_url,
891893
session.extra_login_attributes,

0 commit comments

Comments
 (0)