[java-plugin-shibd-oidc] branch main updated: WIP: Add OIDC claims extraction and agent response
Codeberg
noreply at shibboleth.net
Fri Nov 21 14:23:32 UTC 2025
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-plugin-shibd-oidc.
View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-oidc/commit/282d0eeb3ee4ae071c9ddbcfdbd3ffdb58840444
The following commit(s) were added to refs/heads/main by this push:
new 282d0ee WIP: Add OIDC claims extraction and agent response
282d0ee is described below
commit 282d0eeb3ee4ae071c9ddbcfdbd3ffdb58840444
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Nov 21 14:23:20 2025 +0000
WIP: Add OIDC claims extraction and agent response
- Need to add attribute resolver to claims extraction
- Need to verify the claims extraction logic.
---
.../idp/flows/sp/consumer/oidc/oidc-beans.xml | 4 +
.../idp/flows/sp/consumer/oidc/oidc-flow.xml | 2 +
.../sp/oidc/profile/impl/ExtractOIDCClaims.java | 387 +++++++++++++++++++++
.../sp/oidc/profile/impl/PrepareAgentResponse.java | 35 ++
.../sp/oidc/profile/impl/ProcessEndUserClaims.java | 5 +-
5 files changed, 430 insertions(+), 3 deletions(-)
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
index 07fb82c..f9dff82 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
@@ -550,4 +550,8 @@
</property>
</bean>
+ <bean id="ExtractOIDCClaims" class="net.shibboleth.sp.oidc.profile.impl.ExtractOIDCClaims" scope="prototype"/>
+
+ <bean id="PrepareAgentResponse" class="net.shibboleth.sp.oidc.profile.impl.PrepareAgentResponse" scope="prototype"/>
+
</beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
index afd13c1..6f1e973 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
@@ -99,6 +99,8 @@
<!-- <evaluate expression="ValidateOIDCAuthentication" /> -->
<!-- <evaluate expression="PopulateSubjectCanonicalizationContext" />
<evaluate expression="WriteAuditLog" /> -->
+ <evaluate expression="ExtractOIDCClaims"/>
+ <evaluate expression="PrepareAgentResponse"/>
<evaluate expression="'proceed'" />
<transition on="proceed" to="proceed" />
</action-state>
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExtractOIDCClaims.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExtractOIDCClaims.java
new file mode 100644
index 0000000..0caf134
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExtractOIDCClaims.java
@@ -0,0 +1,387 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.profile.impl;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.context.navigate.RecursiveTypedParentContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.metadata.resolver.MetadataResolver;
+import org.slf4j.Logger;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.minidev.json.JSONObject;
+import net.shibboleth.idp.attribute.AttributeDecodingException;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeSupport;
+import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.shibboleth.idp.attribute.filter.AttributeFilter;
+import net.shibboleth.idp.attribute.filter.AttributeFilterException;
+import net.shibboleth.idp.attribute.filter.context.AttributeFilterContext;
+import net.shibboleth.idp.attribute.filter.context.AttributeFilterContext.Direction;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoder;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry;
+import net.shibboleth.idp.attribute.transcoding.TranscoderSupport;
+import net.shibboleth.idp.attribute.transcoding.TranscodingRule;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.profile.config.OIDCSSORelyingPartyConfiguration;
+import net.shibboleth.oidc.profile.context.EndUserClaimsContext;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
+import net.shibboleth.profile.context.navigate.RelyingPartyIdLookupFunction;
+import net.shibboleth.saml.profile.context.navigate.SAMLMetadataContextLookupFunction;
+import net.shibboleth.shared.annotation.constraint.Live;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.service.ServiceException;
+import net.shibboleth.shared.service.ServiceableComponent;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+
+/**
+ * An action that extracts OIDC End-User claims from the combined id_token and UserInfo response claims in the
+ * {@link EndUserClaimsContext}.
+ */
+public class ExtractOIDCClaims extends AbstractApplicationAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractOIDCClaims.class);
+
+ /** Strategy used to look up {@link EndUserClaimsContext} to operate on. */
+ @Nonnull private Function<ProfileRequestContext,EndUserClaimsContext> endUserClaimsContextLookupStrategy;
+
+ /** Strategy used to look up a {@link RelyingPartyContext} for configuration options. */
+ @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+
+ /** Strategy used to create {@link AttributeContext} to hold results. */
+ @Nonnull private Function<ProfileRequestContext,AttributeContext> attributeContextCreationStrategy;
+
+ /** Context containing the claims(s) to process. */
+ @NonnullBeforeExec private EndUserClaimsContext endUserClaimsContext;
+
+ /** Context for externally supplied inbound attributes. */
+ @NonnullBeforeExec private AttributeContext attributeContext;
+
+ /** Store off profile config. */
+ @NonnullBeforeExec private OIDCSSORelyingPartyConfiguration profileConfiguration;
+
+ /** The set of id_token claims before they are processed.*/
+ @NonnullBeforeExec private JWTClaimsSet unprocessedIdTokenClaims;
+
+ /** Function used to obtain the issuer ID. */
+ @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
+
+ /** Function used to obtain the requester ID. */
+ @Nonnull private Function<ProfileRequestContext,String> requesterLookupStrategy;
+
+ /** Whether to accept decoded attributes that no filter rules applied to. */
+ private boolean acceptUnfilteredAttributes;
+
+ public ExtractOIDCClaims() {
+ relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+
+ // prc -> EndUserClaimsContext
+ endUserClaimsContextLookupStrategy =
+ new ChildContextLookup<>(EndUserClaimsContext.class);
+
+ // PRC -> EndUserClaimsContext -> AttributeContext
+ attributeContextCreationStrategy = new ChildContextLookup<>(AttributeContext.class, true).compose(
+ new ChildContextLookup<>(EndUserClaimsContext.class));
+
+ // These appear reversed because we're referring to the attribute issuer and requester,
+ // which is the inverse of the usual assignment of these labels on an inbound assertion.
+ requesterLookupStrategy = new IssuerLookupFunction();
+ issuerLookupStrategy = new RelyingPartyIdLookupFunction();
+
+ }
+
+ /**
+ * Set the strategy used to return the {@link RelyingPartyContext} for configuration options.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setRelyingPartyContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+ checkSetterPreconditions();
+ relyingPartyContextLookupStrategy =
+ Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to lookup a {@link EndUserClaimsContext}.
+ *
+ * @param strategy the strategy
+ */
+ public void setEndUserClaimsContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, EndUserClaimsContext> strategy) {
+ checkSetterPreconditions();
+
+ endUserClaimsContextLookupStrategy = Constraint.isNotNull(strategy,
+ "EndUserClaimsContextLookupStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to create the {@link AttributeContext} to hold results.
+ *
+ * @param strategy creation strategy
+ */
+ public void setAttributeContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext,AttributeContext> strategy) {
+ checkSetterPreconditions();
+ attributeContextCreationStrategy =
+ Constraint.isNotNull(strategy, "AttributeContext creation strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the attribute requester ID for filtering.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setRequesterLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+ checkSetterPreconditions();
+ requesterLookupStrategy = Constraint.isNotNull(strategy, "Requester lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the attribute issuer ID for filtering.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+ checkSetterPreconditions();
+ issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
+ }
+
+ /**
+ * Set whether to accept decoded {@link IdPAttribute} objects pulled from the assertions if
+ * no filtering rule applied to them.
+ *
+ * <p>This is a variant of the original SP's "wildcard" rule support to allow non-enumerated
+ * attributes to be accepted.</p>
+ *
+ * @param flag
+ */
+ public void setAcceptUnfilteredAttributes(final boolean flag) {
+ checkSetterPreconditions();
+
+ acceptUnfilteredAttributes = flag;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ endUserClaimsContext = endUserClaimsContextLookupStrategy.apply(profileRequestContext);
+ if (endUserClaimsContext == null || endUserClaimsContext.getEndUserClaims() == null ||
+ endUserClaimsContext.getUnprocessedIdTokenClaims() == null) {
+ log.debug("{} No EndUserClaimsContext or Claims available", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ return false;
+ }
+ unprocessedIdTokenClaims = endUserClaimsContext.getUnprocessedIdTokenClaims();
+
+ final RelyingPartyContext rpContext = relyingPartyContextLookupStrategy.apply(profileRequestContext);
+ if (rpContext == null) {
+ log.error("{} Unable to locate RelyingPartyContext", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+ return false;
+ } else if (rpContext.getProfileConfig() == null) {
+ log.error("{} Unable to locate profile configuration", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ } else if (!(rpContext.getProfileConfig() instanceof OIDCSSORelyingPartyConfiguration)) {
+ log.error("{} Not a OIDC RelyingParty profile configuration", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ }
+
+ attributeContext = attributeContextCreationStrategy.apply(profileRequestContext);
+ if (attributeContext == null) {
+ log.debug("{} Unable to create AttributeContext", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ profileConfiguration = (OIDCSSORelyingPartyConfiguration) rpContext.getProfileConfig();
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ processAttributes(profileRequestContext);
+
+ // Acumulator of all the merged results.
+ final Map<String,IdPAttribute> accumulator = new HashMap<>();
+ accumulator.putAll(attributeContext.getIdPAttributes());
+
+ //TODO custom extraction, attribute resolution
+
+ // Install the final result back.
+ attributeContext.setIdPAttributes(accumulator);
+ }
+
+ /**
+ * Process the inbound OIDC Claims.
+ *
+ * @param profileRequestContext current profile request context
+ */
+ private void processAttributes(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ log.debug("{} Decoding incoming OIDC Claims", getLogPrefix());
+
+ final Multimap<String,IdPAttribute> mapped = HashMultimap.create();
+ assert mapped != null;
+
+ final String subject = unprocessedIdTokenClaims.getSubject();
+
+ try (final ServiceableComponent<AttributeTranscoderRegistry> component =
+ ensureApplication().getAttributeTranscoderRegistry().getServiceableComponent()) {
+
+ final ClaimsSet endUserClaims = endUserClaimsContext.getEndUserClaims();
+ assert endUserClaims != null;
+ for (final Map.Entry<String, Object> claim :
+ endUserClaims.toJSONObject().entrySet()) {
+ try {
+ final JSONObject jsonClaim = new JSONObject();
+ jsonClaim.put(claim.getKey(), claim.getValue());
+ decodeAttribute(component.getComponent(), profileRequestContext, jsonClaim, mapped);
+ } catch (final AttributeDecodingException e) {
+ log.error("{} Error decoding inbound claim", getLogPrefix(), e);
+ }
+ }
+
+ } catch (final ServiceException e) {
+ log.error("Attribute transcoder service unavailable", e);
+ return;
+ }
+
+ log.debug("{} Incoming OIDC Claims mapped to attribute IDs: {}", getLogPrefix(), mapped.keySet());
+
+ if (!mapped.isEmpty()) {
+ attributeContext.setUnfilteredIdPAttributes(IdPAttributeSupport.toMapMergeDuplicates(mapped.values()));
+ attributeContext.setIdPAttributes((Map<String,IdPAttribute>) null);
+ filterAttributes(profileRequestContext);
+ }
+ }
+
+ /**
+ * Check for inbound attributes and apply filtering.
+ *
+ * @param profileRequestContext current profile request context
+ */
+ private void filterAttributes(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final AttributeFilterContext filterContext =
+ endUserClaimsContext.ensureSubcontext(AttributeFilterContext.class);
+
+ populateFilterContext(profileRequestContext, filterContext);
+
+ try (final ServiceableComponent<AttributeFilter> filterComponent =
+ ensureApplication().getAttributeFilter().getServiceableComponent();
+ final ServiceableComponent<MetadataResolver> metadataResolverComponent =
+ ensureApplication().getMetadataResolver().getServiceableComponent()) {
+
+ // Populate here for locking scope.
+ filterContext.setMetadataResolver(metadataResolverComponent.getComponent());
+
+ final AttributeFilter filter = filterComponent.getComponent();
+ filter.filterAttributes(filterContext);
+ filterContext.removeFromParent();
+ attributeContext.setIdPAttributes(filterContext.getFilteredIdPAttributes());
+ } catch (final AttributeFilterException e) {
+ log.error("{} Error while filtering inbound attributes", getLogPrefix(), e);
+ } catch (final ServiceException e) {
+ log.error("{} Invalid AttributeFilter configuration", getLogPrefix(), e);
+ }
+ }
+
+ /**
+ * Fill in the filter context data.
+ *
+ * @param profileRequestContext current profile request context
+ * @param filterContext context to populate
+ */
+ private void populateFilterContext(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AttributeFilterContext filterContext) {
+
+ final AttributeContext ac = attributeContext;
+ assert ac != null;
+
+ filterContext.setDirection(Direction.INBOUND)
+ .setPrefilteredIdPAttributes(attributeContext.getUnfilteredIdPAttributes())
+ .setRequesterMetadataContextLookupStrategy(null)
+ .setIssuerMetadataContextLookupStrategy(
+ new SAMLMetadataContextLookupFunction().compose(
+ new RecursiveTypedParentContextLookup<>(ProfileRequestContext.class)))
+ .setProxiedRequesterContextLookupStrategy(null)
+ .setAttributeIssuerID(issuerLookupStrategy.apply(profileRequestContext))
+ .setAttributeRecipientID(requesterLookupStrategy.apply(profileRequestContext))
+ //TODO add this back
+ //.setAttributeRecipientGroupID(profileConfiguration.getAttributeRecipientGroupID(profileRequestContext))
+ .setIncludeUnfilteredAttributes(acceptUnfilteredAttributes);
+ }
+
+ /**
+ * Access the registry of transcoding rules to transform (decode) the input claims to IdP Attributes.
+ *
+ * @param registry registry of transcoding rules
+ * @param profileRequestContext current profile request context
+ * @param input input attribute
+ * @param results collection to add results to
+ *
+ * @throws AttributeDecodingException if a non-ignorable error occurs
+ */
+ private void decodeAttribute(@Nonnull final AttributeTranscoderRegistry registry,
+ @Nonnull final ProfileRequestContext profileRequestContext, @Nonnull final JSONObject input,
+ @Nonnull @NonnullElements @Live final Multimap<String,IdPAttribute> results)
+ throws AttributeDecodingException {
+
+ final Collection<TranscodingRule> transcodingRules = registry.getTranscodingRules(input);
+ if (transcodingRules.isEmpty()) {
+ log.debug("{} No transcoding rule for Attribute '{}'", getLogPrefix(), input);
+ return;
+ }
+
+ for (final TranscodingRule rules : transcodingRules) {
+ assert rules != null;
+ final AttributeTranscoder<JSONObject> transcoder = TranscoderSupport.getTranscoder(rules);
+ final IdPAttribute decodedAttribute = transcoder.decode(profileRequestContext, input, rules);
+ if (decodedAttribute != null) {
+ results.put(decodedAttribute.getId(), decodedAttribute);
+ }
+ }
+ }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareAgentResponse.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareAgentResponse.java
new file mode 100644
index 0000000..7379a31
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareAgentResponse.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.profile.impl;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.sp.profile.AbstractTokenConsumerResponseAction;
+
+/**
+ * OIDC-specific subclass of a token consumer response action.
+ *
+ * TODO...
+ */
+public class PrepareAgentResponse extends AbstractTokenConsumerResponseAction {
+
+ /** {@inheritDoc} */
+ @Override
+ protected String getSessionData(final ProfileRequestContext profileRequestContext) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaims.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaims.java
index 859c88d..6fa289e 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaims.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaims.java
@@ -102,10 +102,9 @@ public class ProcessEndUserClaims extends AbstractProfileAction {
new ChildContextLookup<>(AccessTokenResponseContext.class).compose(
new InboundMessageContextLookup());
- // Will create context.
+ // Will create context. prc -> EndUserClaimsContext
endUserClaimsContextLookupStrategy =
- new ChildContextLookup<>(EndUserClaimsContext.class, true).compose(
- new InboundMessageContextLookup());
+ new ChildContextLookup<>(EndUserClaimsContext.class, true);
claimMergingStrategy = new DefaultClaimMergingStrategy();
claimSanitizationStrategy = new DefaultClaimSanitizationStrategy();
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list