Skip to content

Commit 2e3dcf8

Browse files
committed
oidc_frontend: mirror public subject
Add `sub_mirror_subject` configuration parameter. If this is set to true, the subject received from the backend will be mirrored to the client, if public sub is used. To maintain backwards compatibility, the default value is false. MirrorPublicSubjectIdentifierFactory would normally belong to pyop, but in order to keep the code and the configuration in the same place, this code overloads pyop's HashBasedSubjectIdentifierFactory.
1 parent 385cc09 commit 2e3dcf8

File tree

3 files changed

+40
-3
lines changed

3 files changed

+40
-3
lines changed

doc/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ The configuration parameters available:
433433
* `client_db_uri`: connection URI to MongoDB or Redis instance where the client data will be persistent, if it's not specified the clients list will be received from the `client_db_path`.
434434
* `client_db_path`: path to a file containing the client database in json format. It will only be used if `client_db_uri` is not set. If `client_db_uri` and `client_db_path` are not set, clients will only be stored in-memory (not suitable for production use).
435435
* `sub_hash_salt`: salt which is hashed into the `sub` claim. If it's not specified, SATOSA will generate a random salt on each startup, which means that users will get new `sub` value after every restart.
436+
* `sub_mirror_subject` (default: `No`): if this is set to `Yes` and SATOSA releases a public `sub` claim to the client, then the subject identifier received from the backend will be mirrored to the client. The default is to hash the public subject identifier with `sub_hash_salt`. Pairwise `sub` claims are always hashed.
436437
* `provider`: provider configuration information. MUST be configured, the following configuration are supported:
437438
* `response_types_supported` (default: `[id_token]`): list of all supported response types, see [Section 3 of OIDC Core](http://openid.net/specs/openid-connect-core-1_0.html#Authentication).
438439
* `subject_types_supported` (default: `[pairwise]`): list of all supported subject identifier types, see [Section 8 of OIDC Core](http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes)

src/satosa/frontends/openid_connect.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
logger = logging.getLogger(__name__)
4747

4848

49+
class MirrorPublicSubjectIdentifierFactory(HashBasedSubjectIdentifierFactory):
50+
def create_public_identifier(self, user_id):
51+
return user_id
52+
53+
4954
class OpenIDConnectFrontend(FrontendModule):
5055
"""
5156
A OpenID Connect frontend module
@@ -75,7 +80,10 @@ def __init__(self, auth_req_callback_func, internal_attributes, conf, base_url,
7580
)
7681

7782
sub_hash_salt = self.config.get("sub_hash_salt", rndstr(16))
78-
authz_state = _init_authorization_state(provider_config, db_uri, sub_hash_salt)
83+
mirror_public = self.config.get("sub_mirror_public", False)
84+
authz_state = _init_authorization_state(
85+
provider_config, db_uri, sub_hash_salt, mirror_public
86+
)
7987

8088
client_db_uri = self.config.get("client_db_uri")
8189
cdb_file = self.config.get("client_db_path")
@@ -460,7 +468,9 @@ def _create_provider(
460468
return provider
461469

462470

463-
def _init_authorization_state(provider_config, db_uri, sub_hash_salt):
471+
def _init_authorization_state(
472+
provider_config, db_uri, sub_hash_salt, mirror_public=False
473+
):
464474
if db_uri:
465475
authz_code_db = StorageBase.from_uri(
466476
db_uri,
@@ -499,8 +509,14 @@ def _init_authorization_state(provider_config, db_uri, sub_hash_salt):
499509
]
500510
if k in provider_config
501511
}
512+
513+
subject_id_factory = (
514+
MirrorPublicSubjectIdentifierFactory(sub_hash_salt)
515+
if mirror_public
516+
else HashBasedSubjectIdentifierFactory(sub_hash_salt)
517+
)
502518
return AuthorizationState(
503-
HashBasedSubjectIdentifierFactory(sub_hash_salt),
519+
subject_id_factory,
504520
authz_code_db,
505521
access_token_db,
506522
refresh_token_db,

tests/satosa/frontends/test_openid_connect.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,26 @@ def test_register_endpoints_dynamic_client_registration_is_configurable(
402402
provider_info = ProviderConfigurationResponse().deserialize(frontend.provider_config(None).message, "json")
403403
assert ("registration_endpoint" in provider_info) == client_registration_enabled
404404

405+
@pytest.mark.parametrize("sub_mirror_public", [
406+
True,
407+
False
408+
])
409+
def test_mirrored_subject(self, context, frontend_config, authn_req, sub_mirror_public):
410+
frontend_config["sub_mirror_public"] = sub_mirror_public
411+
frontend_config["provider"]["subject_types_supported"] = ["public"]
412+
frontend = self.create_frontend(frontend_config)
413+
414+
self.insert_client_in_client_db(frontend, authn_req["redirect_uri"])
415+
internal_response = self.setup_for_authn_response(context, frontend, authn_req)
416+
http_resp = frontend.handle_authn_response(context, internal_response)
417+
418+
resp = AuthorizationResponse().deserialize(urlparse(http_resp.message).fragment)
419+
id_token = IdToken().from_jwt(resp["id_token"], key=[frontend.signing_key])
420+
if sub_mirror_public:
421+
assert id_token["sub"] == OIDC_USERS["testuser1"]["eduPersonTargetedID"][0]
422+
else:
423+
assert id_token["sub"] != OIDC_USERS["testuser1"]["eduPersonTargetedID"][0]
424+
405425
def test_token_endpoint(self, context, frontend_config, authn_req):
406426
token_lifetime = 60 * 60 * 24
407427
frontend_config["provider"]["access_token_lifetime"] = token_lifetime

0 commit comments

Comments
 (0)