3535from twisted .web .client import readBody
3636
3737from synapse .config import ConfigError
38+ from synapse .config .oidc_config import OidcProviderConfig
3839from synapse .handlers .sso import MappingException , UserAttributes
3940from synapse .http .site import SynapseRequest
4041from synapse .logging .context import make_deferred_yieldable
7071JWKS = 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+
73199class 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