Skip to content

Optionally fetch userinfo from OIDC IdP - #1358

Open
Jasper51297 wants to merge 4 commits into
uc-cdis:masterfrom
Jasper51297:inaccessible-oidc-attributes-fix
Open

Jasper51297 wants to merge 4 commits into
uc-cdis:masterfrom
Jasper51297:inaccessible-oidc-attributes-fix

Conversation

@Jasper51297

@Jasper51297 Jasper51297 commented Jun 10, 2026 •

Copy link
Copy Markdown

This pull request enables Fence to propagate specific attributes from upstream OIDC Identity Providers (IdPs) into the ID tokens issued to clients. Previously, certain attributes returned by external providers during the OIDC flow were captured by Fence but not made accessible to the downstream applications.

This change introduces a configurable mapping that allows administrators to specify which fields from the upstream userinfo response should be included as claims in the Fence-generated ID token.

https://openid.net/specs/openid-connect-core-1_0.html#UserInfo

Minimal Fence (helm) example:

fence:
  FENCE_CONFIG:
    OPENID_CONNECT:
      sram:
        name: 'SRAM'
        client_id: 'myclientid'
        redirect_url: '{{BASE_URL}}/login/sram/login'
        discovery_url: 'https://proxy.sram.surf.nl/.well-known/openid-configuration'
        scope: 'openid profile email voperson_external_id'
        user_id_field: 'userinfo.email'
        email_field: 'userinfo.email'

New Features

  • Added idp_attr_mapping configuration to OPENID_CONNECT.
  • Support for including upstream IdP attributes in Fence ID tokens.

Breaking Changes

None

Bug Fixes

None

Improvements

  • Improved user session metadata handling to store upstream userinfo.

Dependency updates

None

Deployment changes

None

@nrandorf

Copy link
Copy Markdown

Good afternoon, are you able to address the three failing pytests?

@Jasper51297

Copy link
Copy Markdown
Author

Sorry, did not get notified some tests were failing. This should have been resolved now.

@nrandorf

Copy link
Copy Markdown

Thank you for the update!

raise Exception(e)

if claims.get(user_id_field):
if user_id_field == "email" and not claims.get("email_verified"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You cannot remove this check, it is required when the email field is found and the verified field is found. Google does this to protect against unverified emails

return {"error": "Email is not verified"}
user_id_value = self._get_claim_value(user_id_field, claims, userinfo)

if user_id_value:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this whole return gated behind finding that user_id_value now? Before I believe it just prompted the additional email check?

userinfo_endpoint = self.get_value_from_discovery_doc("userinfo_endpoint", "")
if userinfo_endpoint and raw_access_token:
header = {"Authorization": "Bearer " + raw_access_token}
res = requests.get(userinfo_endpoint, headers=header, proxies=self.get_proxies())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend adding exponential backoff and error handling explicitly since this is an external call.

return res.json()
else:
self.logger.error("Unable to get userinfo: status_code: {}, message: {}".format(res.status_code, res.text))
return {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

failure case return pattern should be avoided. Reverse this logic, fail fast on errors and the default return should be the positive case

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need new unit tests to test the new functionality. Change the config and test that what you added achieves the intended results

"firstname": claims.get(firstname_claim_field),
"lastname": claims.get(lastname_claim_field),
"email": claims.get(email_claim_field),
organization_claim_field: self._get_claim_value(organization_claim_field, claims, userinfo),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could cause implicit overriding of dictionary fields and confuses purpose. organization_claim_field is intended to be the field within the IdP claims to get the organization information from, we are standardizing the location in Gen3's response to "org". Now you are mirroring and duplicating the parsing if the "org" field is in fact "org", e.g. this conflicts with the line explicitly standardizing on "org" below.

Can you please explain more context around why this is required? Can you not simply provide this existing configuration to get the org information from the right claim and then parse the "org" from the "org" field?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants