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

Commit 55d7457

Browse files
committed
Split OidcProvider out of OidcHandler
The idea here is that we will have an instance of OidcProvider for each configured IdP, with OidcHandler just doing the marshalling of them. For now it's still hardcoded with a single provider.
1 parent fa4e504 commit 55d7457

File tree

3 files changed

+187
-156
lines changed

3 files changed

+187
-156
lines changed

synapse/app/homeserver.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,6 @@ async def start():
429429
oidc = hs.get_oidc_handler()
430430
# Loading the provider metadata also ensures the provider config is valid.
431431
await oidc.load_metadata()
432-
await oidc.load_jwks()
433432

434433
await _base.start(hs, config.listeners)
435434

synapse/handlers/oidc_handler.py

Lines changed: 139 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from twisted.web.client import readBody
3636

3737
from synapse.config import ConfigError
38+
from synapse.config.oidc_config import OidcProviderConfig
3839
from synapse.handlers.sso import MappingException, UserAttributes
3940
from synapse.http.site import SynapseRequest
4041
from synapse.logging.context import make_deferred_yieldable
@@ -70,6 +71,131 @@
7071
JWKS = TypedDict("JWKS", {"keys": List[JWK]})
7172

7273

74+
class OidcHandler:
75+
"""Handles requests related to the OpenID Connect login flow.
76+
"""
77+
78+
def __init__(self, hs: "HomeServer"):
79+
self._sso_handler = hs.get_sso_handler()
80+
81+
provider_conf = hs.config.oidc.oidc_provider
82+
# we should not have been instantiated if there is no configured provider.
83+
assert provider_conf is not None
84+
85+
self._token_generator = OidcSessionTokenGenerator(hs)
86+
87+
self._provider = OidcProvider(hs, self._token_generator, provider_conf)
88+
89+
async def load_metadata(self) -> None:
90+
"""Validate the config and load the metadata from the remote endpoint.
91+
92+
Called at startup to ensure we have everything we need.
93+
"""
94+
await self._provider.load_metadata()
95+
await self._provider.load_jwks()
96+
97+
async def handle_oidc_callback(self, request: SynapseRequest) -> None:
98+
"""Handle an incoming request to /_synapse/oidc/callback
99+
100+
Since we might want to display OIDC-related errors in a user-friendly
101+
way, we don't raise SynapseError from here. Instead, we call
102+
``self._sso_handler.render_error`` which displays an HTML page for the error.
103+
104+
Most of the OpenID Connect logic happens here:
105+
106+
- first, we check if there was any error returned by the provider and
107+
display it
108+
- then we fetch the session cookie, decode and verify it
109+
- the ``state`` query parameter should match with the one stored in the
110+
session cookie
111+
112+
Once we know the session is legit, we then delegate to the OIDC Provider
113+
implementation, which will exchange the code with the provider and complete the
114+
login/authentication.
115+
116+
Args:
117+
request: the incoming request from the browser.
118+
"""
119+
120+
# The provider might redirect with an error.
121+
# In that case, just display it as-is.
122+
if b"error" in request.args:
123+
# error response from the auth server. see:
124+
# https://tools.ietf.org/html/rfc6749#section-4.1.2.1
125+
# https://openid.net/specs/openid-connect-core-1_0.html#AuthError
126+
error = request.args[b"error"][0].decode()
127+
description = request.args.get(b"error_description", [b""])[0].decode()
128+
129+
# Most of the errors returned by the provider could be due by
130+
# either the provider misbehaving or Synapse being misconfigured.
131+
# The only exception of that is "access_denied", where the user
132+
# probably cancelled the login flow. In other cases, log those errors.
133+
if error != "access_denied":
134+
logger.error("Error from the OIDC provider: %s %s", error, description)
135+
136+
self._sso_handler.render_error(request, error, description)
137+
return
138+
139+
# otherwise, it is presumably a successful response. see:
140+
# https://tools.ietf.org/html/rfc6749#section-4.1.2
141+
142+
# Fetch the session cookie
143+
session = request.getCookie(SESSION_COOKIE_NAME) # type: Optional[bytes]
144+
if session is None:
145+
logger.info("No session cookie found")
146+
self._sso_handler.render_error(
147+
request, "missing_session", "No session cookie found"
148+
)
149+
return
150+
151+
# Remove the cookie. There is a good chance that if the callback failed
152+
# once, it will fail next time and the code will already be exchanged.
153+
# Removing it early avoids spamming the provider with token requests.
154+
request.addCookie(
155+
SESSION_COOKIE_NAME,
156+
b"",
157+
path="/_synapse/oidc",
158+
expires="Thu, Jan 01 1970 00:00:00 UTC",
159+
httpOnly=True,
160+
sameSite="lax",
161+
)
162+
163+
# Check for the state query parameter
164+
if b"state" not in request.args:
165+
logger.info("State parameter is missing")
166+
self._sso_handler.render_error(
167+
request, "invalid_request", "State parameter is missing"
168+
)
169+
return
170+
171+
state = request.args[b"state"][0].decode()
172+
173+
# Deserialize the session token and verify it.
174+
try:
175+
session_data = self._token_generator.verify_oidc_session_token(
176+
session, state
177+
)
178+
except MacaroonDeserializationException as e:
179+
logger.exception("Invalid session")
180+
self._sso_handler.render_error(request, "invalid_session", str(e))
181+
return
182+
except MacaroonInvalidSignatureException as e:
183+
logger.exception("Could not verify session")
184+
self._sso_handler.render_error(request, "mismatching_session", str(e))
185+
return
186+
187+
if b"code" not in request.args:
188+
logger.info("Code parameter is missing")
189+
self._sso_handler.render_error(
190+
request, "invalid_request", "Code parameter is missing"
191+
)
192+
return
193+
194+
code = request.args[b"code"][0].decode()
195+
196+
await self._provider.handle_oidc_callback(request, session_data, code)
197+
198+
73199
class OidcError(Exception):
74200
"""Used to catch errors when calling the token_endpoint
75201
"""
@@ -84,21 +210,25 @@ def __str__(self):
84210
return self.error
85211

86212

87-
class OidcHandler:
88-
"""Handles requests related to the OpenID Connect login flow.
213+
class OidcProvider:
214+
"""Wraps the config for a single OIDC IdentityProvider
215+
216+
Provides methods for handling redirect requests and callbacks via that particular
217+
IdP.
89218
"""
90219

91-
def __init__(self, hs: "HomeServer"):
220+
def __init__(
221+
self,
222+
hs: "HomeServer",
223+
token_generator: "OidcSessionTokenGenerator",
224+
provider: OidcProviderConfig,
225+
):
92226
self._store = hs.get_datastore()
93227

94-
self._token_generator = OidcSessionTokenGenerator(hs)
228+
self._token_generator = token_generator
95229

96230
self._callback_url = hs.config.oidc_callback_url # type: str
97231

98-
provider = hs.config.oidc.oidc_provider
99-
# we should not have been instantiated if there is no configured provider.
100-
assert provider is not None
101-
102232
self._scopes = provider.scopes
103233
self._user_profile_method = provider.user_profile_method
104234
self._client_auth = ClientAuth(
@@ -552,108 +682,7 @@ async def handle_redirect_request(
552682
nonce=nonce,
553683
)
554684

555-
async def handle_oidc_callback(self, request: SynapseRequest) -> None:
556-
"""Handle an incoming request to /_synapse/oidc/callback
557-
558-
Since we might want to display OIDC-related errors in a user-friendly
559-
way, we don't raise SynapseError from here. Instead, we call
560-
``self._sso_handler.render_error`` which displays an HTML page for the error.
561-
562-
Most of the OpenID Connect logic happens here:
563-
564-
- first, we check if there was any error returned by the provider and
565-
display it
566-
- then we fetch the session cookie, decode and verify it
567-
- the ``state`` query parameter should match with the one stored in the
568-
session cookie
569-
570-
Once we know the session is legit, we then then ddelegate to
571-
_handle_oidc_callback_for_provider, which will exchange the code with the
572-
provider and complete the login/authentication.
573-
574-
Args:
575-
request: the incoming request from the browser.
576-
"""
577-
578-
# The provider might redirect with an error.
579-
# In that case, just display it as-is.
580-
if b"error" in request.args:
581-
# error response from the auth server. see:
582-
# https://tools.ietf.org/html/rfc6749#section-4.1.2.1
583-
# https://openid.net/specs/openid-connect-core-1_0.html#AuthError
584-
error = request.args[b"error"][0].decode()
585-
description = request.args.get(b"error_description", [b""])[0].decode()
586-
587-
# Most of the errors returned by the provider could be due by
588-
# either the provider misbehaving or Synapse being misconfigured.
589-
# The only exception of that is "access_denied", where the user
590-
# probably cancelled the login flow. In other cases, log those errors.
591-
if error != "access_denied":
592-
logger.error("Error from the OIDC provider: %s %s", error, description)
593-
594-
self._sso_handler.render_error(request, error, description)
595-
return
596-
597-
# otherwise, it is presumably a successful response. see:
598-
# https://tools.ietf.org/html/rfc6749#section-4.1.2
599-
600-
# Fetch the session cookie
601-
session = request.getCookie(SESSION_COOKIE_NAME) # type: Optional[bytes]
602-
if session is None:
603-
logger.info("No session cookie found")
604-
self._sso_handler.render_error(
605-
request, "missing_session", "No session cookie found"
606-
)
607-
return
608-
609-
# Remove the cookie. There is a good chance that if the callback failed
610-
# once, it will fail next time and the code will already be exchanged.
611-
# Removing it early avoids spamming the provider with token requests.
612-
request.addCookie(
613-
SESSION_COOKIE_NAME,
614-
b"",
615-
path="/_synapse/oidc",
616-
expires="Thu, Jan 01 1970 00:00:00 UTC",
617-
httpOnly=True,
618-
sameSite="lax",
619-
)
620-
621-
# Check for the state query parameter
622-
if b"state" not in request.args:
623-
logger.info("State parameter is missing")
624-
self._sso_handler.render_error(
625-
request, "invalid_request", "State parameter is missing"
626-
)
627-
return
628-
629-
state = request.args[b"state"][0].decode()
630-
631-
# Deserialize the session token and verify it.
632-
try:
633-
session_data = self._token_generator.verify_oidc_session_token(
634-
session, state
635-
)
636-
except MacaroonDeserializationException as e:
637-
logger.exception("Invalid session")
638-
self._sso_handler.render_error(request, "invalid_session", str(e))
639-
return
640-
except MacaroonInvalidSignatureException as e:
641-
logger.exception("Could not verify session")
642-
self._sso_handler.render_error(request, "mismatching_session", str(e))
643-
return
644-
645-
if b"code" not in request.args:
646-
logger.info("Code parameter is missing")
647-
self._sso_handler.render_error(
648-
request, "invalid_request", "Code parameter is missing"
649-
)
650-
return
651-
652-
code = request.args[b"code"][0].decode()
653-
654-
await self._handle_oidc_callback_for_provider(request, session_data, code)
655-
656-
async def _handle_oidc_callback_for_provider(
685+
async def handle_oidc_callback(
657686
self, request: SynapseRequest, session_data: "OidcSessionData", code: str
658687
) -> None:
659688
"""Handle an incoming request to /_synapse/oidc/callback

0 commit comments

Comments
 (0)