Skip to content

Commit

Permalink
oidc_frontend: mirror public subject
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bajnokk committed Aug 26, 2022
1 parent 385cc09 commit 9423981
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 3 deletions.
1 change: 1 addition & 0 deletions doc/README.md
Expand Up @@ -433,6 +433,7 @@ The configuration parameters available:
* `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`.
* `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).
* `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.
* `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.
* `provider`: provider configuration information. MUST be configured, the following configuration are supported:
* `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).
* `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)
Expand Down
20 changes: 17 additions & 3 deletions src/satosa/frontends/openid_connect.py
Expand Up @@ -46,6 +46,11 @@
logger = logging.getLogger(__name__)


class MirrorPublicSubjectIdentifierFactory(HashBasedSubjectIdentifierFactory):
def create_public_identifier(self, user_id):
return user_id


class OpenIDConnectFrontend(FrontendModule):
"""
A OpenID Connect frontend module
Expand Down Expand Up @@ -75,7 +80,10 @@ def __init__(self, auth_req_callback_func, internal_attributes, conf, base_url,
)

sub_hash_salt = self.config.get("sub_hash_salt", rndstr(16))
authz_state = _init_authorization_state(provider_config, db_uri, sub_hash_salt)
mirror_public = self.config.get("sub_mirror_public", False)
authz_state = _init_authorization_state(
provider_config, db_uri, sub_hash_salt, mirror_public
)

client_db_uri = self.config.get("client_db_uri")
cdb_file = self.config.get("client_db_path")
Expand Down Expand Up @@ -460,7 +468,7 @@ def _create_provider(
return provider


def _init_authorization_state(provider_config, db_uri, sub_hash_salt):
def _init_authorization_state(provider_config, db_uri, sub_hash_salt, mirror_public):
if db_uri:
authz_code_db = StorageBase.from_uri(
db_uri,
Expand Down Expand Up @@ -499,8 +507,14 @@ def _init_authorization_state(provider_config, db_uri, sub_hash_salt):
]
if k in provider_config
}

subject_id_factory = (
MirrorPublicSubjectIdentifierFactory(sub_hash_salt)
if mirror_public
else HashBasedSubjectIdentifierFactory(sub_hash_salt)
)
return AuthorizationState(
HashBasedSubjectIdentifierFactory(sub_hash_salt),
subject_id_factory,
authz_code_db,
access_token_db,
refresh_token_db,
Expand Down
21 changes: 21 additions & 0 deletions tests/satosa/frontends/test_openid_connect.py
Expand Up @@ -402,6 +402,27 @@ def test_register_endpoints_dynamic_client_registration_is_configurable(
provider_info = ProviderConfigurationResponse().deserialize(frontend.provider_config(None).message, "json")
assert ("registration_endpoint" in provider_info) == client_registration_enabled

@pytest.mark.parametrize("sub_mirror_public", [
True,
False
])
def test_mirrored_subject(self, context, frontend_config, authn_req, sub_mirror_public):
frontend_config["sub_mirror_public"] = sub_mirror_public
frontend_config["provider"]["subject_types_supported"] = ["public"]
frontend = self.create_frontend(frontend_config)

self.insert_client_in_client_db(frontend, authn_req["redirect_uri"])
internal_response = self.setup_for_authn_response(context, frontend, authn_req)
http_resp = frontend.handle_authn_response(context, internal_response)

resp = AuthorizationResponse().deserialize(urlparse(http_resp.message).fragment)
id_token = IdToken().from_jwt(resp["id_token"], key=[frontend.signing_key])
assert "sub" in id_token
if sub_mirror_public:
assert id_token["sub"] == OIDC_USERS["testuser1"]["eduPersonTargetedID"][0]
else:
assert id_token["sub"] != OIDC_USERS["testuser1"]["eduPersonTargetedID"][0]

def test_token_endpoint(self, context, frontend_config, authn_req):
token_lifetime = 60 * 60 * 24
frontend_config["provider"]["access_token_lifetime"] = token_lifetime
Expand Down

0 comments on commit 9423981

Please sign in to comment.