Skip to content

Commit 11edb70

Browse files
authored
Merge pull request #34 from its-dirg/oidc-frontend-fix
Update OIDCFrontend
2 parents 891e7aa + 59dbe69 commit 11edb70

File tree

3 files changed

+51
-22
lines changed

3 files changed

+51
-22
lines changed

src/satosa/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ def _auth_req_callback_func(self, context, internal_request):
105105
context.state[consent.STATE_KEY] = {"filter": internal_request.approved_attributes or []}
106106
satosa_logging(logger, logging.INFO,
107107
"Requesting provider: {}".format(internal_request.requester), state)
108-
context.request = None
109108

110109
UserIdHasher.save_state(internal_request, state)
111110
if self.request_micro_services:
@@ -115,6 +114,7 @@ def _auth_req_callback_func(self, context, internal_request):
115114

116115
def _auth_req_finish(self, context, internal_request):
117116
backend = self.module_router.backend_routing(context)
117+
context.request = None
118118
return backend.start_auth(context, internal_request)
119119

120120
def _auth_resp_finish(self, context, internal_response):

src/satosa/frontends/openid_connect.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44
import json
55
import logging
6-
from urllib.parse import urlencode
6+
from urllib.parse import urlencode, urlparse
77

88
from jwkest.jwk import rsa_load, RSAKey
99
from oic.oic.message import (AuthorizationRequest, AuthorizationErrorResponse, TokenErrorResponse,
@@ -57,6 +57,7 @@ def _create_provider(self, endpoint_baseurl):
5757
capabilities = {
5858
"issuer": self.base_url,
5959
"authorization_endpoint": "{}/{}".format(endpoint_baseurl, AuthorizationEndpoint.url),
60+
"jwks_uri": "{}/jwks".format(endpoint_baseurl),
6061
"response_types_supported": response_types_supported,
6162
"id_token_signing_alg_values_supported": [self.signing_key.alg],
6263
"response_modes_supported": ["fragment", "query"],
@@ -102,7 +103,7 @@ def _init_authorization_state(self):
102103
return AuthorizationState(HashBasedSubjectIdentifierFactory(sub_hash_salt), authz_code_db, access_token_db,
103104
refresh_token_db, sub_db, **token_lifetimes)
104105

105-
def handle_authn_response(self, context, internal_resp):
106+
def handle_authn_response(self, context, internal_resp, extra_id_token_claims=None):
106107
"""
107108
See super class method satosa.frontends.base.FrontendModule#handle_authn_response
108109
:type context: satosa.context.Context
@@ -114,7 +115,7 @@ def handle_authn_response(self, context, internal_resp):
114115

115116
attributes = self.converter.from_internal("openid", internal_resp.attributes)
116117
self.user_db[internal_resp.user_id] = {k: v[0] for k, v in attributes.items()}
117-
auth_resp = self.provider.authorize(auth_req, internal_resp.user_id)
118+
auth_resp = self.provider.authorize(auth_req, internal_resp.user_id, extra_id_token_claims)
118119

119120
del context.state[self.name]
120121
http_response = auth_resp.request(auth_req["redirect_uri"], should_fragment_encode(auth_req))
@@ -138,32 +139,46 @@ def register_endpoints(self, backend_names):
138139
:rtype: list[(str, ((satosa.context.Context, Any) -> satosa.response.Response, Any))]
139140
:raise ValueError: if more than one backend is configured
140141
"""
142+
backend_name = None
141143
if len(backend_names) != 1:
142144
# only supports one backend since there currently is no way to publish multiple authorization endpoints
143145
# in configuration information and there is no other standard way of authorization_endpoint discovery
144146
# similar to SAML entity discovery
145-
raise ValueError("OpenID Connect frontend only supports one backend.")
146-
backend = backend_names[0]
147-
endpoint_baseurl = "{}/{}".format(self.base_url, backend)
147+
# this can be circumvented with a custom RequestMicroService which handles the routing based on something
148+
# in the authentication request
149+
logger.warn("More than one backend is configured, make sure to provide a custom routing micro service to "
150+
"determine which backend should be used per request.")
151+
else:
152+
backend_name = backend_names[0]
153+
154+
endpoint_baseurl = "{}/{}".format(self.base_url, self.name)
148155
self._create_provider(endpoint_baseurl)
149156

150157
provider_config = ("^.well-known/openid-configuration$", self.provider_config)
151-
jwks_uri = ("^jwks$", self.jwks)
152-
authentication = ("^{}/{}".format(backend, AuthorizationEndpoint.url), self.handle_authn_request)
158+
jwks_uri = ("^{}/jwks$".format(self.name), self.jwks)
159+
160+
if backend_name:
161+
# if there is only one backend, include its name in the path so the default routing can work
162+
auth_endpoint = "{}/{}/{}/{}".format(self.base_url, backend_name, self.name, AuthorizationEndpoint.url)
163+
self.provider.configuration_information["authorization_endpoint"] = auth_endpoint
164+
auth_path = urlparse(auth_endpoint).path.lstrip("/")
165+
else:
166+
auth_path = "{}/{}".format(self.name, AuthorizationEndpoint.url)
167+
authentication = ("^{}$".format(auth_path), self.handle_authn_request)
153168
url_map = [provider_config, jwks_uri, authentication]
154169

155170
if any("code" in v for v in self.provider.configuration_information["response_types_supported"]):
156171
self.provider.configuration_information["token_endpoint"] = "{}/{}".format(endpoint_baseurl,
157172
TokenEndpoint.url)
158-
token_endpoint = ("^{}/{}".format(backend, TokenEndpoint.url), self.token_endpoint)
173+
token_endpoint = ("^{}/{}".format(self.name, TokenEndpoint.url), self.token_endpoint)
159174
url_map.append(token_endpoint)
160175

161176
self.provider.configuration_information["userinfo_endpoint"] = "{}/{}".format(endpoint_baseurl,
162177
UserinfoEndpoint.url)
163-
userinfo_endpoint = ("^{}/{}".format(backend, UserinfoEndpoint.url), self.userinfo_endpoint)
178+
userinfo_endpoint = ("^{}/{}".format(self.name, UserinfoEndpoint.url), self.userinfo_endpoint)
164179
url_map.append(userinfo_endpoint)
165180
if "registration_endpoint" in self.provider.configuration_information:
166-
client_registration = ("^{}/{}".format(backend, RegistrationEndpoint.url), self.client_registration)
181+
client_registration = ("^{}/{}".format(self.name, RegistrationEndpoint.url), self.client_registration)
167182
url_map.append(client_registration)
168183

169184
return url_map
@@ -247,8 +262,17 @@ def handle_authn_request(self, context):
247262
client_id = authn_req["client_id"]
248263
context.state[self.name] = {"oidc_request": request}
249264
hash_type = oidc_subject_type_to_hash_type(self.provider.clients[client_id].get("subject_type", "pairwise"))
250-
internal_req = InternalRequest(hash_type, client_id, self.provider.clients[client_id].get("client_name"))
265+
client_name = self.provider.clients[client_id].get("client_name")
266+
if client_name:
267+
# TODO should process client names for all languages, see OIDC Registration, Section 2.1
268+
requester_name = [{"lang": "en", "text": client_name}]
269+
else:
270+
requester_name = None
271+
internal_req = InternalRequest(hash_type, client_id, requester_name)
251272

273+
internal_req.approved_attributes = self.converter.to_internal_filter("openid",
274+
self.provider.configuration_information[
275+
"claims_supported"])
252276
return self.auth_req_callback_func(context, internal_req)
253277

254278
def jwks(self, context):

tests/satosa/frontends/test_openid_connect.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from tests.users import USERS
2222

2323
INTERNAL_ATTRIBUTES = {
24-
'attributes': {"mail": {"saml": ["email"], "openid": ["email"]}}
24+
"attributes": {"mail": {"saml": ["email"], "openid": ["email"]}}
2525
}
2626
BASE_URL = "https://op.example.com"
2727
CLIENT_ID = "client1"
@@ -60,11 +60,12 @@ def authn_req(self):
6060
nonce=nonce, claims=claims_req)
6161
return req
6262

63-
def insert_client_in_client_db(self, frontend, redirect_uri):
63+
def insert_client_in_client_db(self, frontend, redirect_uri, extra_metadata={}):
6464
frontend.provider.clients = {
6565
CLIENT_ID: {"response_types": ["code", "id_token"],
6666
"redirect_uris": [redirect_uri],
6767
"client_secret": CLIENT_SECRET}}
68+
frontend.provider.clients[CLIENT_ID].update(extra_metadata)
6869

6970
def insert_user_in_user_db(self, frontend, user_id):
7071
frontend.user_db[user_id] = {"email": "[email protected]"}
@@ -104,15 +105,18 @@ def test_handle_authn_response(self, context, frontend, authn_req):
104105
def test_handle_authn_request(self, context, frontend, authn_req):
105106
mock_callback = Mock()
106107
frontend.auth_req_callback_func = mock_callback
107-
self.insert_client_in_client_db(frontend, authn_req["redirect_uri"])
108+
client_name = "test client"
109+
self.insert_client_in_client_db(frontend, authn_req["redirect_uri"], {"client_name": client_name})
108110

109111
context.request = dict(parse_qsl(authn_req.to_urlencoded()))
110112
frontend.handle_authn_request(context)
111113

112114
assert mock_callback.call_count == 1
113115
context, internal_req = mock_callback.call_args[0]
114116
assert internal_req.requester == authn_req["client_id"]
117+
assert internal_req.requester_name == [{"lang": "en", "text": client_name}]
115118
assert internal_req.user_id_hash_type == UserIdHashType.pairwise
119+
assert internal_req.approved_attributes == ["mail"]
116120

117121
def test_handle_backend_error(self, context, frontend):
118122
redirect_uri = "https://client.example.com"
@@ -158,22 +162,23 @@ def test_register_client_with_wrong_response_type(self, context, frontend):
158162
def test_provider_configuration_endpoint(self, context, frontend):
159163
expected_capabilities = {
160164
"response_types_supported": ["code", "id_token", "code id_token token"],
161-
"token_endpoint": BASE_URL + "/foo_backend/token",
165+
"jwks_uri": "{}/{}/jwks".format(BASE_URL, frontend.name),
166+
"authorization_endpoint": "{}/foo_backend/{}/authorization".format(BASE_URL, frontend.name),
167+
"token_endpoint": "{}/{}/token".format(BASE_URL, frontend.name),
168+
"userinfo_endpoint": "{}/{}/userinfo".format(BASE_URL, frontend.name),
162169
"id_token_signing_alg_values_supported": ["RS256"],
163170
"response_modes_supported": ["fragment", "query"],
164171
"subject_types_supported": ["pairwise"],
165172
"claim_types_supported": ["normal"],
166173
"claims_parameter_supported": True,
167174
"request_parameter_supported": False,
168175
"request_uri_parameter_supported": False,
169-
"authorization_endpoint": "{}/foo_backend/authorization".format(BASE_URL),
170176
"scopes_supported": ["openid", "email"],
171177
"claims_supported": ["email"],
172178
"grant_types_supported": ["authorization_code", "implicit"],
173179
"issuer": BASE_URL,
174180
"require_request_uri_registration": True,
175181
"token_endpoint_auth_methods_supported": ["client_secret_basic"],
176-
"userinfo_endpoint": "{}/foo_backend/userinfo".format(BASE_URL),
177182
"version": "3.0"
178183
}
179184

@@ -189,8 +194,8 @@ def test_jwks(self, context, frontend):
189194

190195
def test_register_endpoints_token_and_userinfo_endpoint_is_published_if_necessary(self, frontend):
191196
urls = frontend.register_endpoints(["test"])
192-
assert ("^{}/{}".format("test", TokenEndpoint.url), frontend.token_endpoint) in urls
193-
assert ("^{}/{}".format("test", UserinfoEndpoint.url), frontend.userinfo_endpoint) in urls
197+
assert ("^{}/{}".format(frontend.name, TokenEndpoint.url), frontend.token_endpoint) in urls
198+
assert ("^{}/{}".format(frontend.name, UserinfoEndpoint.url), frontend.userinfo_endpoint) in urls
194199

195200
def test_register_endpoints_token_and_userinfo_endpoint_is_not_published_if_only_implicit_flow(
196201
self, frontend_config, context):
@@ -215,7 +220,7 @@ def test_register_endpoints_dynamic_client_registration_is_configurable(
215220
frontend = self.frontend(frontend_config)
216221

217222
urls = frontend.register_endpoints(["test"])
218-
assert (("^{}/{}".format("test", RegistrationEndpoint.url),
223+
assert (("^{}/{}".format(frontend.name, RegistrationEndpoint.url),
219224
frontend.client_registration) in urls) == client_registration_enabled
220225
provider_info = ProviderConfigurationResponse().deserialize(frontend.provider_config(None).message, "json")
221226
assert ("registration_endpoint" in provider_info) == client_registration_enabled

0 commit comments

Comments
 (0)