mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2025-05-03 16:14:49 -04:00
Support UI Authentication for OpenID Connect accounts (#7457)
This commit is contained in:
parent
03aff4c75e
commit
a3cf36f76e
6 changed files with 105 additions and 41 deletions
|
@ -311,7 +311,7 @@ class OidcHandler:
|
|||
``ClientAuth`` to authenticate with the client with its ID and secret.
|
||||
|
||||
Args:
|
||||
code: The autorization code we got from the callback.
|
||||
code: The authorization code we got from the callback.
|
||||
|
||||
Returns:
|
||||
A dict containing various tokens.
|
||||
|
@ -497,11 +497,14 @@ class OidcHandler:
|
|||
return UserInfo(claims)
|
||||
|
||||
async def handle_redirect_request(
|
||||
self, request: SynapseRequest, client_redirect_url: bytes
|
||||
) -> None:
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
client_redirect_url: bytes,
|
||||
ui_auth_session_id: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Handle an incoming request to /login/sso/redirect
|
||||
|
||||
It redirects the browser to the authorization endpoint with a few
|
||||
It returns a redirect to the authorization endpoint with a few
|
||||
parameters:
|
||||
|
||||
- ``client_id``: the client ID set in ``oidc_config.client_id``
|
||||
|
@ -511,24 +514,32 @@ class OidcHandler:
|
|||
- ``state``: a random string
|
||||
- ``nonce``: a random string
|
||||
|
||||
In addition to redirecting the client, we are setting a cookie with
|
||||
In addition generating a redirect URL, we are setting a cookie with
|
||||
a signed macaroon token containing the state, the nonce and the
|
||||
client_redirect_url params. Those are then checked when the client
|
||||
comes back from the provider.
|
||||
|
||||
|
||||
Args:
|
||||
request: the incoming request from the browser.
|
||||
We'll respond to it with a redirect and a cookie.
|
||||
client_redirect_url: the URL that we should redirect the client to
|
||||
when everything is done
|
||||
ui_auth_session_id: The session ID of the ongoing UI Auth (or
|
||||
None if this is a login).
|
||||
|
||||
Returns:
|
||||
The redirect URL to the authorization endpoint.
|
||||
|
||||
"""
|
||||
|
||||
state = generate_token()
|
||||
nonce = generate_token()
|
||||
|
||||
cookie = self._generate_oidc_session_token(
|
||||
state=state, nonce=nonce, client_redirect_url=client_redirect_url.decode(),
|
||||
state=state,
|
||||
nonce=nonce,
|
||||
client_redirect_url=client_redirect_url.decode(),
|
||||
ui_auth_session_id=ui_auth_session_id,
|
||||
)
|
||||
request.addCookie(
|
||||
SESSION_COOKIE_NAME,
|
||||
|
@ -541,7 +552,7 @@ class OidcHandler:
|
|||
|
||||
metadata = await self.load_metadata()
|
||||
authorization_endpoint = metadata.get("authorization_endpoint")
|
||||
uri = prepare_grant_uri(
|
||||
return prepare_grant_uri(
|
||||
authorization_endpoint,
|
||||
client_id=self._client_auth.client_id,
|
||||
response_type="code",
|
||||
|
@ -550,8 +561,6 @@ class OidcHandler:
|
|||
state=state,
|
||||
nonce=nonce,
|
||||
)
|
||||
request.redirect(uri)
|
||||
finish_request(request)
|
||||
|
||||
async def handle_oidc_callback(self, request: SynapseRequest) -> None:
|
||||
"""Handle an incoming request to /_synapse/oidc/callback
|
||||
|
@ -625,7 +634,11 @@ class OidcHandler:
|
|||
|
||||
# Deserialize the session token and verify it.
|
||||
try:
|
||||
nonce, client_redirect_url = self._verify_oidc_session_token(session, state)
|
||||
(
|
||||
nonce,
|
||||
client_redirect_url,
|
||||
ui_auth_session_id,
|
||||
) = self._verify_oidc_session_token(session, state)
|
||||
except MacaroonDeserializationException as e:
|
||||
logger.exception("Invalid session")
|
||||
self._render_error(request, "invalid_session", str(e))
|
||||
|
@ -678,15 +691,21 @@ class OidcHandler:
|
|||
return
|
||||
|
||||
# and finally complete the login
|
||||
await self._auth_handler.complete_sso_login(
|
||||
user_id, request, client_redirect_url
|
||||
)
|
||||
if ui_auth_session_id:
|
||||
await self._auth_handler.complete_sso_ui_auth(
|
||||
user_id, ui_auth_session_id, request
|
||||
)
|
||||
else:
|
||||
await self._auth_handler.complete_sso_login(
|
||||
user_id, request, client_redirect_url
|
||||
)
|
||||
|
||||
def _generate_oidc_session_token(
|
||||
self,
|
||||
state: str,
|
||||
nonce: str,
|
||||
client_redirect_url: str,
|
||||
ui_auth_session_id: Optional[str],
|
||||
duration_in_ms: int = (60 * 60 * 1000),
|
||||
) -> str:
|
||||
"""Generates a signed token storing data about an OIDC session.
|
||||
|
@ -702,6 +721,8 @@ class OidcHandler:
|
|||
nonce: The ``nonce`` parameter passed to the OIDC provider.
|
||||
client_redirect_url: The URL the client gave when it initiated the
|
||||
flow.
|
||||
ui_auth_session_id: The session ID of the ongoing UI Auth (or
|
||||
None if this is a login).
|
||||
duration_in_ms: An optional duration for the token in milliseconds.
|
||||
Defaults to an hour.
|
||||
|
||||
|
@ -718,12 +739,19 @@ class OidcHandler:
|
|||
macaroon.add_first_party_caveat(
|
||||
"client_redirect_url = %s" % (client_redirect_url,)
|
||||
)
|
||||
if ui_auth_session_id:
|
||||
macaroon.add_first_party_caveat(
|
||||
"ui_auth_session_id = %s" % (ui_auth_session_id,)
|
||||
)
|
||||
now = self._clock.time_msec()
|
||||
expiry = now + duration_in_ms
|
||||
macaroon.add_first_party_caveat("time < %d" % (expiry,))
|
||||
|
||||
return macaroon.serialize()
|
||||
|
||||
def _verify_oidc_session_token(self, session: str, state: str) -> Tuple[str, str]:
|
||||
def _verify_oidc_session_token(
|
||||
self, session: str, state: str
|
||||
) -> Tuple[str, str, Optional[str]]:
|
||||
"""Verifies and extract an OIDC session token.
|
||||
|
||||
This verifies that a given session token was issued by this homeserver
|
||||
|
@ -734,7 +762,7 @@ class OidcHandler:
|
|||
state: The state the OIDC provider gave back
|
||||
|
||||
Returns:
|
||||
The nonce and the client_redirect_url for this session
|
||||
The nonce, client_redirect_url, and ui_auth_session_id for this session
|
||||
"""
|
||||
macaroon = pymacaroons.Macaroon.deserialize(session)
|
||||
|
||||
|
@ -744,17 +772,27 @@ class OidcHandler:
|
|||
v.satisfy_exact("state = %s" % (state,))
|
||||
v.satisfy_general(lambda c: c.startswith("nonce = "))
|
||||
v.satisfy_general(lambda c: c.startswith("client_redirect_url = "))
|
||||
# Sometimes there's a UI auth session ID, it seems to be OK to attempt
|
||||
# to always satisfy this.
|
||||
v.satisfy_general(lambda c: c.startswith("ui_auth_session_id = "))
|
||||
v.satisfy_general(self._verify_expiry)
|
||||
|
||||
v.verify(macaroon, self._macaroon_secret_key)
|
||||
|
||||
# Extract the `nonce` and `client_redirect_url` from the token
|
||||
# Extract the `nonce`, `client_redirect_url`, and maybe the
|
||||
# `ui_auth_session_id` from the token.
|
||||
nonce = self._get_value_from_macaroon(macaroon, "nonce")
|
||||
client_redirect_url = self._get_value_from_macaroon(
|
||||
macaroon, "client_redirect_url"
|
||||
)
|
||||
try:
|
||||
ui_auth_session_id = self._get_value_from_macaroon(
|
||||
macaroon, "ui_auth_session_id"
|
||||
) # type: Optional[str]
|
||||
except ValueError:
|
||||
ui_auth_session_id = None
|
||||
|
||||
return nonce, client_redirect_url
|
||||
return nonce, client_redirect_url, ui_auth_session_id
|
||||
|
||||
def _get_value_from_macaroon(self, macaroon: pymacaroons.Macaroon, key: str) -> str:
|
||||
"""Extracts a caveat value from a macaroon token.
|
||||
|
@ -773,7 +811,7 @@ class OidcHandler:
|
|||
for caveat in macaroon.caveats:
|
||||
if caveat.caveat_id.startswith(prefix):
|
||||
return caveat.caveat_id[len(prefix) :]
|
||||
raise Exception("No %s caveat in macaroon" % (key,))
|
||||
raise ValueError("No %s caveat in macaroon" % (key,))
|
||||
|
||||
def _verify_expiry(self, caveat: str) -> bool:
|
||||
prefix = "time < "
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue