[java-idp-oidc] branch main updated: JOIDC-11 - Support for client_credentials grant

Scott Cantor cantor.2 at osu.edu
Wed Feb 9 16:45:33 UTC 2022


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch main
in repository java-idp-oidc.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=4568cc735ef46fba6eb4eb2cfc22fbf497889eac

The following commit(s) were added to refs/heads/main by this push:
     new 4568cc73 JOIDC-11 - Support for client_credentials grant
4568cc73 is described below

commit 4568cc735ef46fba6eb4eb2cfc22fbf497889eac
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Feb 9 11:45:30 2022 -0500

    JOIDC-11 - Support for client_credentials grant
    
    https://shibboleth.atlassian.net/browse/JOIDC-11
    
    Redesign that begins to handle unregistered clients/resource servers.
    Encryption support done but untested.
    Reworked optional encryption into a proper config option.
    Moved scope/audience validation more fully to metadata for now.
---
 .../navigate/AudienceClientIDLookupFunction.java   |  55 +++++
 .../context/OAuthAttributeResolutionContext.java   |  67 ------
 .../navigate/ClientInfoAudienceLookupFunction.java |  66 ++++++
 .../DefaultOIDCMetadataContextLookupFunction.java  |  30 ++-
 ...ava => TokenRequestAudienceLookupFunction.java} |  33 ++-
 .../oidc/op/profile/context/package-info.java      |  21 --
 .../impl/PopulateOIDCMetadataContext.java          |  57 ++---
 .../impl/SetEntityIdToSAMLPeerEntityContext.java   |  28 +--
 .../profile/impl/AbstractEncryptTokenAction.java}  |  69 +++---
 .../op/oauth2/profile/impl/BuildAccessToken.java   | 173 +--------------
 ...esponseContext.java => EncryptAccessToken.java} |  69 +++---
 .../profile/impl/EnforceAudienceConfiguration.java | 171 +++++++++++++++
 .../OAuthAttributeResolutionContextDecorator.java  |  70 ------
 .../impl/SetAccessTokenToResponseContext.java      |  12 +-
 .../op/oauth2/profile/impl/ValidateAudience.java   | 182 +++++++++++++++
 .../AbstractOIDCAuthenticationResponseAction.java  |   2 +-
 .../op/profile/impl/EncryptProcessedToken.java     | 129 ++---------
 .../impl/InitializeRelyingPartyContext.java        |  35 ++-
 .../op/profile/impl/OIDCMetadataLookupHandler.java |  11 +-
 .../impl/PopulateOIDCEncryptionParameters.java     | 107 ++++++---
 .../PopulateOIDCSignatureSigningParameters.java    |   3 +-
 .../plugin/oidc/op/profile/impl/ValidateScope.java |  22 +-
 ...entInformationEncryptionParametersResolver.java |  22 +-
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  13 +-
 .../flows/oidc/abstract/oidc-abstract-beans.xml    |   1 -
 .../idp/flows/oidc/authorize/authorize-beans.xml   |   2 +-
 .../oidc/metadata-lookup/metadata-lookup-beans.xml |  79 ++++---
 .../idp/flows/oidc/token/token-beans.xml           | 242 +++++++++++++++++---
 .../shibboleth/idp/flows/oidc/token/token-flow.xml | 103 +++++++--
 .../idp/flows/oidc/userinfo/userinfo-beans.xml     |   4 +-
 .../idp/service/relying-party/postconfig.xml       |  30 ++-
 .../SetEntityIdToSAMLPeerEntityContextTest.java    |  69 +++---
 .../oauth2/profile/impl/BuildAccessTokenTest.java  | 243 +--------------------
 .../oauth2/profile/impl/ValidateAudienceTest.java  | 219 +++++++++++++++++++
 .../oidc/op/profile/flow/AbstractOidcFlowTest.java |   1 +
 .../flow/ClientCredentialsTokenFlowTest.java       |   2 -
 .../profile/impl/BaseOIDCResponseActionTest.java   |   3 +-
 .../op/profile/impl/EncryptProcessedTokenTest.java |   7 +-
 .../impl/PopulateOIDCEncryptionParametersTest.java |  22 +-
 .../oidc/op/profile/impl/ValidateScopeTest.java    |   8 +-
 .../src/test/resources/conf/attribute-resolver.xml |  25 ---
 .../src/test/resources/conf/oidc.properties        |   3 -
 .../src/test/resources/conf/relying-party.xml      |   4 +-
 43 files changed, 1464 insertions(+), 1050 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/AudienceClientIDLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/AudienceClientIDLookupFunction.java
new file mode 100644
index 00000000..394e4c31
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/navigate/AudienceClientIDLookupFunction.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.plugin.oidc.op.messaging.context.navigate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+
+/**
+ * Returns a {@link ClientID} from the first value found in {@link OIDCAuthenticationResponseContext#getAudience()}.
+ * 
+ *  @since 3.1.0
+ */
+public class AudienceClientIDLookupFunction implements ContextDataLookupFunction<MessageContext,ClientID> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(AudienceClientIDLookupFunction.class);
+    
+    /** {@inheritDoc} */
+    @Nullable public ClientID apply(@Nullable final MessageContext input) {
+        if (input != null) {
+            final OIDCAuthenticationResponseContext respCtx =
+                    input.getSubcontext(OIDCAuthenticationResponseContext.class);
+            if (respCtx != null && !respCtx.getAudience().isEmpty()) {
+                return new ClientID(respCtx.getAudience().get(0));
+            }
+        }
+        
+        return null;
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/OAuthAttributeResolutionContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/OAuthAttributeResolutionContext.java
deleted file mode 100644
index 0bcf6d2f..00000000
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/OAuthAttributeResolutionContext.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You 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.idp.plugin.oidc.op.profile.context;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-import javax.annotation.Nonnull;
-
-import org.opensaml.messaging.context.BaseContext;
-
-import net.shibboleth.utilities.java.support.annotation.constraint.Live;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
-
-/**
- * A supplemental context adding OAuth- and OIDC-related input to the attribute resolution process.
- * 
- * @since 3.1.0
- */
-public final class OAuthAttributeResolutionContext extends BaseContext {
-
-    /** Requested and validated scopes. */
-    @Nonnull @NonnullElements private final Collection<String> scopes;
-
-    /** Requested resources. */
-    @Nonnull @NonnullElements private final Collection<String> resources;
-    
-    /** Constructor. */
-    public OAuthAttributeResolutionContext() {
-        scopes = new ArrayList<>();
-        resources = new ArrayList<>();
-    }
-    
-    /**
-     * Get the requested and metadata-validated scope collection for this request.
-     * 
-     * @return modifiable collection of scope values
-     */
-    @Nonnull @NonnullElements @Live public Collection<String> getScope() {
-        return scopes;
-    }
-
-    /**
-     * Get the requested audience collection for this request.
-     * 
-     * @return modifiable collection of audience values
-     */
-    @Nonnull @NonnullElements @Live public Collection<String> getResources() {
-        return resources;
-    }
-
-}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ClientInfoAudienceLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ClientInfoAudienceLookupFunction.java
new file mode 100644
index 00000000..0e24f991
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ClientInfoAudienceLookupFunction.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.plugin.oidc.op.profile.context.navigate;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+
+/** A function that returns {@link OIDCMetadataContext}. */
+public class ClientInfoAudienceLookupFunction implements ContextDataLookupFunction<OIDCMetadataContext,List<String>> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ClientInfoAudienceLookupFunction.class);
+    
+    /** {@inheritDoc} */
+    @Nullable @NonnullElements
+    public List<String> apply(@Nullable final OIDCMetadataContext input) {
+        if (input == null || input.getClientInformation() == null) {
+            return null;
+        }
+    
+        final Object obj = input.getClientInformation().getMetadata().getCustomField("audience");
+        if (obj instanceof Collection) {
+            final List<String> audience = new ArrayList<>();
+            
+            for (final Object val: (Collection) obj) {
+                if (val instanceof String) {
+                    audience.add((String) val);
+                } else if (val instanceof URI) {
+                    audience.add(val.toString());
+                }
+            }
+            
+            return audience;
+        }
+        
+        return null;
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultOIDCMetadataContextLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultOIDCMetadataContextLookupFunction.java
index bf16f50c..cc4ac730 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultOIDCMetadataContextLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultOIDCMetadataContextLookupFunction.java
@@ -19,6 +19,7 @@ package net.shibboleth.idp.plugin.oidc.op.profile.context.navigate;
 
 import javax.annotation.Nullable;
 
+import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
 import org.opensaml.profile.context.ProfileRequestContext;
 
@@ -28,13 +29,38 @@ import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
 public class DefaultOIDCMetadataContextLookupFunction implements
         ContextDataLookupFunction<ProfileRequestContext, OIDCMetadataContext> {
 
+    /** Use the inbound message context. */
+    private boolean inbound;
+    
+    /** Constructor. */
+    public DefaultOIDCMetadataContextLookupFunction() {
+        inbound = true;
+    }
+    
+    /**
+     * Sets whether to pull the subcontext from the inbound message context.
+     * 
+     * <p>Default is true. False means to use the outbound context.</p>
+     * 
+     * @param flag
+     */
+    public void setInbound(final boolean flag) {
+        inbound = flag;
+    }
+    
     /** {@inheritDoc} */
     @Nullable
     public OIDCMetadataContext apply(@Nullable final ProfileRequestContext input) {
-        if (input == null || input.getInboundMessageContext() == null) {
+        if (input == null) {
+            return null;
+        }
+        
+        final MessageContext mc = inbound ? input.getInboundMessageContext() : input.getOutboundMessageContext();
+        if (mc == null) {
             return null;
         }
-        return input.getInboundMessageContext().getSubcontext(OIDCMetadataContext.class, false);
+        
+        return mc.getSubcontext(OIDCMetadataContext.class);
     }
 
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultOIDCMetadataContextLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestAudienceLookupFunction.java
similarity index 53%
copy from idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultOIDCMetadataContextLookupFunction.java
copy to idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestAudienceLookupFunction.java
index bf16f50c..fb4add50 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultOIDCMetadataContextLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestAudienceLookupFunction.java
@@ -17,24 +17,35 @@
 
 package net.shibboleth.idp.plugin.oidc.op.profile.context.navigate;
 
+import java.net.URI;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
-import org.opensaml.profile.context.ProfileRequestContext;
+import com.nimbusds.oauth2.sdk.TokenRequest;
 
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 
-/** A function that returns {@link OIDCMetadataContext}. */
-public class DefaultOIDCMetadataContextLookupFunction implements
-        ContextDataLookupFunction<ProfileRequestContext, OIDCMetadataContext> {
+/**
+ * A function that returns a copy of requested resource list from a {@link TokenRequest}.
+ */
+public class TokenRequestAudienceLookupFunction extends AbstractTokenRequestLookupFunction<List<String>> {
 
     /** {@inheritDoc} */
-    @Nullable
-    public OIDCMetadataContext apply(@Nullable final ProfileRequestContext input) {
-        if (input == null || input.getInboundMessageContext() == null) {
-            return null;
+    @Override
+    @Nullable @NonnullElements List<String> doLookup(@Nonnull final TokenRequest req) {
+
+        if (req != null) {
+            final List<URI> uris = req.getResources();
+            if (uris != null) {
+                return uris.stream()
+                        .map(URI::toString)
+                        .collect(Collectors.toUnmodifiableList());
+            }
         }
-        return input.getInboundMessageContext().getSubcontext(OIDCMetadataContext.class, false);
+        return null;
     }
 
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/package-info.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/package-info.java
deleted file mode 100644
index 7fa5371a..00000000
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/package-info.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You 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.
- */
-
-/**
- * Contexts related to OP profile actions.
- */
-package net.shibboleth.idp.plugin.oidc.op.profile.context;
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCMetadataContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/impl/PopulateOIDCMetadataContext.java
similarity index 65%
rename from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCMetadataContext.java
rename to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/impl/PopulateOIDCMetadataContext.java
index 3bea04f6..bd3c7878 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCMetadataContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/impl/PopulateOIDCMetadataContext.java
@@ -15,19 +15,18 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+package net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.impl;
 
 import java.util.List;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.MessageContext;
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.action.EventIds;
-import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.handler.AbstractMessageHandler;
 import org.opensaml.saml.common.messaging.context.SAMLMetadataContext;
+import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
 import org.opensaml.saml.saml2.metadata.RoleDescriptor;
 import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
 import org.slf4j.Logger;
@@ -36,34 +35,29 @@ import org.slf4j.LoggerFactory;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
-import net.shibboleth.idp.profile.AbstractProfileAction;
-import net.shibboleth.idp.saml.profile.context.navigate.SAMLMetadataContextLookupFunction;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
- * An action that attempts to locate OIDC client information from the SAML entity descriptor containing role
+ * A message handler that attempts to locate OIDC client information from the SAML entity descriptor containing role
  * descriptor of type {@link SPSSODescriptor}. If it contains {@link OIDCClientInformation} in the
- * object metadata, it is attached inside {@link OIDCMetadataContext} as a child of a pre-existing instance of
- * inbound {@link MessageContext}.
+ * object metadata, it is attached inside {@link OIDCMetadataContext} as a child of the input {@link MessageContext}.
  */
-public class PopulateOIDCMetadataContext extends AbstractProfileAction {
+public class PopulateOIDCMetadataContext extends AbstractMessageHandler {
 
     /** Class logger. */
     @Nonnull
     private final Logger log = LoggerFactory.getLogger(PopulateOIDCMetadataContext.class);
     
     /** Set the strategy used to look up the {@link SAMLMetadataContext} to draw from. */
-    @Nonnull private Function<ProfileRequestContext, SAMLMetadataContext> samlMetadataContextLookupStrategy;
-    
-    /** SAML metadata context to populate from. */
-    @Nullable private SAMLMetadataContext samlMetadataCtx;
+    @Nonnull private Function<MessageContext,SAMLMetadataContext> samlMetadataContextLookupStrategy;
     
     /**
      * Constructor.
      */
     public PopulateOIDCMetadataContext() {
-        samlMetadataContextLookupStrategy = new SAMLMetadataContextLookupFunction();
+        samlMetadataContextLookupStrategy = new ChildContextLookup<>(SAMLMetadataContext.class).compose(
+                new ChildContextLookup<>(SAMLPeerEntityContext.class));
     }
     
     /**
@@ -71,31 +65,24 @@ public class PopulateOIDCMetadataContext extends AbstractProfileAction {
      * 
      * @param strategy strategy used to look up the {@link SAMLMetadataContext}
      */
-    public void setSamlMetadataContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext,SAMLMetadataContext> strategy) {
+    public void setSAMLMetadataContextLookupStrategy(
+            @Nonnull final Function<MessageContext,SAMLMetadataContext> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
 
         samlMetadataContextLookupStrategy =
                 Constraint.isNotNull(strategy, "SAMLMetadataContext lookup strategy cannot be null");
     }
-
+    
     /** {@inheritDoc} */
     @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+    protected void doInvoke(@Nonnull final MessageContext messageContext) {
         
-        samlMetadataCtx = samlMetadataContextLookupStrategy.apply(profileRequestContext);
+        final SAMLMetadataContext samlMetadataCtx = samlMetadataContextLookupStrategy.apply(messageContext);
         if (samlMetadataCtx == null) {
-            log.debug("{} Unable to locate SAMLMetadataContext", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
+            log.error("Unable to locate SAMLMetadataContext");
+            return;
         }
-        
-        return super.doPreExecute(profileRequestContext);
-    }
-    
-    /** {@inheritDoc} */
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
         final List<RoleDescriptor> roleDescriptors 
             = samlMetadataCtx.getEntityDescriptor().getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
         for (final RoleDescriptor roleDescriptor : roleDescriptors) {
@@ -105,12 +92,14 @@ public class PopulateOIDCMetadataContext extends AbstractProfileAction {
                 if (clientInformations != null && clientInformations.size() > 0) {
                     final OIDCMetadataContext oidcCtx = new OIDCMetadataContext();
                     oidcCtx.setClientInformation(clientInformations.get(0));
-                    profileRequestContext.getInboundMessageContext().addSubcontext(oidcCtx);
-                    log.debug("{} Client information found and attached.", getLogPrefix());
+                    messageContext.addSubcontext(oidcCtx);
+                    log.debug("{} Client information found and attached", getLogPrefix());
                     return;
                 }   
             }
         }
+        
         log.debug("{} No client information found to be attached into OIDC metadata context.", getLogPrefix());
     }
-}
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetEntityIdToSAMLPeerEntityContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/impl/SetEntityIdToSAMLPeerEntityContext.java
similarity index 80%
rename from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetEntityIdToSAMLPeerEntityContext.java
rename to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/impl/SetEntityIdToSAMLPeerEntityContext.java
index 1faef605..055be821 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetEntityIdToSAMLPeerEntityContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/impl/SetEntityIdToSAMLPeerEntityContext.java
@@ -15,16 +15,16 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+package net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.impl;
 
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 
 import org.opensaml.messaging.context.MessageContext;
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.action.EventIds;
-import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.messaging.handler.AbstractMessageHandler;
+import org.opensaml.messaging.handler.MessageHandler;
+import org.opensaml.messaging.handler.MessageHandlerException;
 import org.opensaml.saml.common.messaging.context.AbstractSAMLEntityContext;
 import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
 import org.slf4j.Logger;
@@ -33,15 +33,14 @@ import org.slf4j.LoggerFactory;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 
 import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultClientIDLookupFunction;
-import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
- * Action that sets the entityID to the given {@link SAMLPeerEntityContext} class, found as the subcontext of the
- * inbound message context. The value to be set is resolved via the given lookup strategy for client ID.
+ * {@link MessageHandler} that sets the entityID to the given {@link SAMLPeerEntityContext} class.
+ * The value to be set is resolved via the given lookup strategy for client ID.
  */
-public class SetEntityIdToSAMLPeerEntityContext extends AbstractProfileAction {
+public class SetEntityIdToSAMLPeerEntityContext extends AbstractMessageHandler {
 
     /** Class logger. */
     @Nonnull
@@ -87,13 +86,13 @@ public class SetEntityIdToSAMLPeerEntityContext extends AbstractProfileAction {
     
     /** {@inheritDoc} */
     @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        final MessageContext messageContext = profileRequestContext.getInboundMessageContext();
-        final AbstractSAMLEntityContext entityCtx =  messageContext.getSubcontext(entityContextClass);
+    protected void doInvoke(@Nonnull final MessageContext messageContext) throws MessageHandlerException {
+        
+        final AbstractSAMLEntityContext entityCtx = messageContext.getSubcontext(entityContextClass);
         if (entityCtx == null) {
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
-            return;
+            throw new MessageHandlerException("Unable to locate subcontext of type " + entityContextClass);
         }
+        
         final ClientID clientID = clientIDLookupStrategy.apply(messageContext);
         if (clientID != null) {
             log.debug("{} Set clientID '{}' to the peer entity context", getLogPrefix(), clientID.getValue());
@@ -102,4 +101,5 @@ public class SetEntityIdToSAMLPeerEntityContext extends AbstractProfileAction {
             log.debug("{} No clientID could be resolved, nothing to do", getLogPrefix());
         }
     }
-}
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/EncryptProcessedToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/AbstractEncryptTokenAction.java
similarity index 74%
copy from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/EncryptProcessedToken.java
copy to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/AbstractEncryptTokenAction.java
index 83cd30a3..1b077f13 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/EncryptProcessedToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/AbstractEncryptTokenAction.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
 
 import java.security.interfaces.ECPublicKey;
 import java.security.interfaces.RSAPublicKey;
@@ -45,40 +45,35 @@ import com.nimbusds.jose.crypto.AESEncrypter;
 import com.nimbusds.jose.crypto.ECDHEncrypter;
 import com.nimbusds.jose.crypto.RSAEncrypter;
 import com.nimbusds.jwt.EncryptedJWT;
-import com.nimbusds.jwt.SignedJWT;
 
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction;
 import net.shibboleth.idp.profile.context.RelyingPartyContext;
 import net.shibboleth.oidc.security.impl.CredentialConversionUtil;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
- * Action that serves both id token and user info response encryption. Existence of encryption parameters is taken as
- * indication whether the encryption should take place. Action assumes the content to be encrypted is located primarily
- * by {@link OIDCAuthenticationResponseContext#getProcessedToken()} returning either signed id token or signed user info
- * response. If such information is not available action assumes the data to be encrypted is
- * {@link OIDCAuthenticationResponseContext#getUserInfo()} containing bare user info response. If neither of the sources
- * for encryption exists the actions fails.
+ * Action that encrypts a source object into an {@link EncryptedJWT}.
+ * 
+ * <p> The existence of encryption parameters is used to decide whether the encryption should take place.
+ * 
+ * @since 3.1.0
  */
-public class EncryptProcessedToken extends AbstractOIDCResponseAction {
+public abstract class AbstractEncryptTokenAction extends AbstractOIDCResponseAction {
 
     /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(EncryptProcessedToken.class);
+    @Nonnull private Logger log = LoggerFactory.getLogger(AbstractEncryptTokenAction.class);
 
     /** Strategy used to look up the {@link EncryptionContext} to store parameters in. */
-    @Nonnull private Function<ProfileRequestContext, EncryptionContext> encryptionContextLookupStrategy;
+    @Nonnull private Function<ProfileRequestContext,EncryptionContext> encryptionContextLookupStrategy;
 
     /** Encryption parameters for encrypting payload. */
     @Nullable private EncryptionParameters params;
 
-    /** Payload to encrypt. */
-    @Nullable private Payload payload;
-
     /**
      * Constructor.
      */
-    public EncryptProcessedToken() {
+    public AbstractEncryptTokenAction() {
         encryptionContextLookupStrategy = new ChildContextLookup<>(EncryptionContext.class).compose(
                 new ChildContextLookup<>(RelyingPartyContext.class));
     }
@@ -89,7 +84,7 @@ public class EncryptProcessedToken extends AbstractOIDCResponseAction {
      * @param strategy lookup strategy
      */
     public void setEncryptionContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, EncryptionContext> strategy) {
+            @Nonnull final Function<ProfileRequestContext,EncryptionContext> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
 
         encryptionContextLookupStrategy =
@@ -102,7 +97,7 @@ public class EncryptProcessedToken extends AbstractOIDCResponseAction {
         if (!super.doPreExecute(profileRequestContext)) {
             return false;
         }
-        
+
         final EncryptionContext encryptCtx = encryptionContextLookupStrategy.apply(profileRequestContext);
         if (encryptCtx == null) {
             log.error("{} No EncryptionContext returned by lookup strategy", getLogPrefix());
@@ -114,16 +109,6 @@ public class EncryptProcessedToken extends AbstractOIDCResponseAction {
             log.debug("{} No Encryption parameters, nothing to do", getLogPrefix());
             return false;
         }
-        if (getOidcResponseContext().getProcessedToken() != null) {
-            payload = new Payload((SignedJWT) getOidcResponseContext().getProcessedToken());
-        } else if (getOidcResponseContext().getUserInfo() != null) {
-            payload = new Payload(getOidcResponseContext().getUserInfo().toJSONObject());
-        }
-        if (payload == null) {
-            log.error("{} Instructed to encrypt but no plain text source available", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
-            return false;
-        }
         
         return true;
     }
@@ -132,6 +117,12 @@ public class EncryptProcessedToken extends AbstractOIDCResponseAction {
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
+        final Payload payload = getPayload(profileRequestContext);
+        if (payload == null) {
+            log.debug("{} No plain text source provided to encrypt", getLogPrefix());
+            return;
+        }
+        
         final JWEAlgorithm encAlg = JWEAlgorithm.parse(params.getKeyTransportEncryptionAlgorithm());
         final Credential credential = params.getKeyTransportEncryptionCredential();
         final EncryptionMethod encEnc = EncryptionMethod.parse(params.getDataEncryptionAlgorithm());
@@ -153,11 +144,29 @@ public class EncryptProcessedToken extends AbstractOIDCResponseAction {
                 log.error("{} Unsupported algorithm {}", getLogPrefix(), encAlg.getName());
                 ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
             }
-            getOidcResponseContext().setProcessedToken(EncryptedJWT.parse(jweObject.serialize()));
+            setProcessedToken(profileRequestContext, EncryptedJWT.parse(jweObject.serialize()));
         } catch (final JOSEException | ParseException e) {
-            log.error("{} Encryption failed {}", getLogPrefix(), e.getMessage());
+            log.error("{} Encryption failed {}", getLogPrefix(), e);
             ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
         }
     }
 
+    /**
+     * Get the payload to encrypt.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return payload to encrypt
+     */
+    @Nonnull protected abstract Payload getPayload(@Nonnull final ProfileRequestContext profileRequestContext);
+    
+    /**
+     * Store the resulting token.
+     * 
+     * @param profileRequestContext profile request context
+     * @param token encrypted token
+     */
+    protected abstract void setProcessedToken(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final EncryptedJWT token);
+            
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
index 27b4c694..ba01d36c 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
@@ -17,12 +17,9 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
 
-import java.net.URI;
 import java.time.Duration;
 import java.time.Instant;
-import java.util.Collection;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -36,11 +33,8 @@ import org.slf4j.LoggerFactory;
 
 import com.nimbusds.jwt.PlainJWT;
 import com.nimbusds.oauth2.sdk.Scope;
-import com.nimbusds.oauth2.sdk.TokenRequest;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 
-import net.shibboleth.idp.attribute.IdPAttribute;
-import net.shibboleth.idp.attribute.StringAttributeValue;
 import net.shibboleth.idp.attribute.context.AttributeContext;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.AccessTokenContext;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
@@ -49,7 +43,6 @@ import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet.Builder;
 import net.shibboleth.idp.profile.IdPEventIds;
-import net.shibboleth.idp.profile.context.RelyingPartyContext;
 import net.shibboleth.idp.profile.context.navigate.ResponderIdLookupFunction;
 import net.shibboleth.oidc.profile.config.navigate.AccessTokenLifetimeLookupFunction;
 import net.shibboleth.oidc.profile.config.navigate.AccessTokenTypeLookupFunction;
@@ -57,12 +50,9 @@ import net.shibboleth.oidc.profile.config.navigate.AccessTokenTypeLookupFunction
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
-import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.logic.FunctionSupport;
-import net.shibboleth.utilities.java.support.primitive.StringSupport;
 import net.shibboleth.utilities.java.support.security.DataSealer;
 import net.shibboleth.utilities.java.support.security.DataSealerException;
 import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrategy;
@@ -113,18 +103,6 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
 
     /** Strategy used to create the subcontext to hold the token. */
     @Nonnull private Function<ProfileRequestContext,AccessTokenContext> accessTokenContextCreationStrategy;
-
-    /** Strategy used to locate the attribute source for scope/audience. */
-    @Nonnull private Function<ProfileRequestContext,AttributeContext> attributeContextLookupStrategy;
-    
-    /** Source scope/audience from unfiltered attributes. */
-    private boolean useUnfilteredAttributes;
-    
-    /** ID of attribute to populate scope from. */
-    @Nullable @NotEmpty private String scopeAttribute;
-    
-    /** ID of attribute to populate audience from. */
-    @Nullable @NotEmpty private String audienceAttribute;
     
     /** Use a JWT for the token. */
     private boolean jwtTokenType;
@@ -138,9 +116,6 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
     /** The generator to use. */
     @Nullable private IdentifierGenerationStrategy idGenerator;
     
-    /** Token request. */
-    @Nullable private TokenRequest tokenRequest;
-    
     /** Constructor. */
     public BuildAccessToken() {
         accessTokenTypeLookupStrategy = new AccessTokenTypeLookupFunction();
@@ -156,14 +131,6 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         accessTokenContextCreationStrategy = new ChildContextLookup<>(AccessTokenContext.class, true).compose(
                 new ChildContextLookup<>(OIDCAuthenticationResponseContext.class).compose(
                         new OutboundMessageContextLookup()));
-        
-        // PRC -> RPC -> AC
-        attributeContextLookupStrategy = new ChildContextLookup<>(AttributeContext.class).compose(
-                new ChildContextLookup<>(RelyingPartyContext.class));
-        
-        useUnfilteredAttributes = true;
-        scopeAttribute = "scope";
-        audienceAttribute = "audience";
     }
     
     /**
@@ -226,20 +193,6 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         accessTokenContextCreationStrategy =
                 Constraint.isNotNull(strategy, "AccessTokenContext creation strategy cannot be null");
     }
-
-    /**
-     * Set the strategy used to locate the {@link AttributeContext} associated with a given
-     * {@link ProfileRequestContext}.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setAttributeContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext,AttributeContext> strategy) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
-        attributeContextLookupStrategy =
-                Constraint.isNotNull(strategy, "AttributeContext lookup strategy cannot be null");
-    }
     
     /**
      * Set the strategy used to locate the issuer value to use.
@@ -262,48 +215,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         
         clientIDLookupStrategy = Constraint.isNotNull(strategy, "ClientID lookup strategy cannot be null");
     }
-    
-    /**
-     * Set whether to source the scope and audience claims from unfiltered attributes.
-     * 
-     * <p>Default is true.</p>
-     * 
-     * @param flag flag to set
-     */
-    public void setUseUnfilteredAttributes(final boolean flag) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        
-        useUnfilteredAttributes = flag;
-    }
-    
-    /**
-     * Set the ID of an {@link IdPAttribute} to source the scope claim.
-     * 
-     * <p>If unset or no values can be extracted, the validated scopes from the request are used.</p>
-     * 
-     * @param id attribute ID
-     */
-    public void setScopeAttribute(@Nullable @NotEmpty final String id) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        
-        scopeAttribute = StringSupport.trimOrNull(id);
-    }
 
-    /**
-     * Set the ID of an {@link IdPAttribute} to source the audience claim.
-     * 
-     * <p>If set, an attribute must be available to pull the values from. If unset, the requested resource
-     * values are used.</p>
-     * 
-     * @param id attribute ID
-     */
-    public void setAudienceAttribute(@Nullable @NotEmpty final String id) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        
-        audienceAttribute = StringSupport.trimOrNull(id);
-    }
-
-// Checkstyle: CyclomaticComplexity OFF
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -311,14 +223,6 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
             return false;
         }
         
-        if (profileRequestContext.getInboundMessageContext().getMessage() instanceof TokenRequest) {
-            tokenRequest = (TokenRequest) profileRequestContext.getInboundMessageContext().getMessage();
-        } else {
-            log.error("{} No inbound TokenRequest message", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
-            return false;
-        }
-        
         final String tokenType = accessTokenTypeLookupStrategy.apply(profileRequestContext);
         jwtTokenType = tokenType != null && "JWT".equals(tokenType);
         
@@ -342,13 +246,6 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
             return false;
         }
         
-        attributeCtx = attributeContextLookupStrategy.apply(profileRequestContext);
-        if (attributeCtx == null && (scopeAttribute != null || audienceAttribute != null)) {
-            log.error("{} Unable to locate AttributeContext", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_ATTRIBUTE_CTX);
-            return false;
-        }
-        
         final Duration lifetime = accessTokenLifetimeLookupStrategy.apply(profileRequestContext);
         if (lifetime == null) {
             log.error("{} No lifetime supplied for access token", getLogPrefix());
@@ -356,10 +253,8 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
             return false;
         }
         accessTokenCtx.setLifetime(lifetime);
-        
         return true;
     }
-// Checkstyle: CyclomaticComplexity ON
 
     /** {@inheritDoc} */
     @Override
@@ -375,16 +270,9 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         
         final OIDCAuthenticationResponseContext responseCtx = getOidcResponseContext();
 
-        final Collection<String> audience = getAudience();
-        if (audience == null || audience.isEmpty()) {
-            log.warn("{} Unable to determine audience value, failing request", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
-            return;
-        }
-        responseCtx.getAudience().addAll(audience);
-        log.debug("{} Building access token with audience: {}", getLogPrefix(), audience);
-
-        final Scope scope = getScope(responseCtx.getScope());
+        final Scope scope = responseCtx.getScope() != null ? responseCtx.getScope() : new Scope();
+        
+        log.debug("{} Building access token with audience: {}", getLogPrefix(), responseCtx.getAudience());
         log.debug("{} Building access token with scope: {}", getLogPrefix(), scope);
 
         final Instant now = Instant.now();
@@ -400,7 +288,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
                 .setACR(responseCtx.getAcr())
                 .setAuthenticationTime(responseCtx.getAuthTime())
                 .setScope(scope)
-                .setAudience(audience);
+                .setAudience(responseCtx.getAudience());
         
         if (jwtTokenType && responseCtx.getAccessTokenClaimSet() != null) {
             builder.setCustomClaims(responseCtx.getAccessTokenClaimSet().toJSONObject());
@@ -423,56 +311,5 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
             ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
         }
     }
-    
-    /**
-     * Produce the desired {@link Scope} claim for the access token.
-     * 
-     * @param validatedScope requested scope(s) that are valid according to client's metadata
-     * 
-     * @return derived {@link Scope} to use or null
-     */
-    @Nonnull private Scope getScope(@Nullable final Scope validatedScope) {
-        
-        if (scopeAttribute != null) {
-            final IdPAttribute source = (useUnfilteredAttributes ? attributeCtx.getUnfilteredIdPAttributes()
-                    : attributeCtx.getIdPAttributes()).get(scopeAttribute);
-            if (source != null) {
-                return Scope.parse(
-                        source.getValues().stream()
-                            .filter(StringAttributeValue.class::isInstance)
-                            .map(StringAttributeValue.class::cast)
-                            .map(StringAttributeValue::getValue)
-                            .collect(Collectors.toUnmodifiableList()));
-            }
-        }
-        
-        log.debug("{} Using originally requested/validated scope", getLogPrefix());
-        return validatedScope != null ? validatedScope : new Scope();
-    }
-    
-    @Nullable @NonnullElements private Collection<String> getAudience() {
-        if (audienceAttribute != null) {
-            final IdPAttribute source = (useUnfilteredAttributes ? attributeCtx.getUnfilteredIdPAttributes()
-                    : attributeCtx.getIdPAttributes()).get(audienceAttribute);
-            if (source != null) {
-                return source.getValues().stream()
-                        .filter(StringAttributeValue.class::isInstance)
-                        .map(StringAttributeValue.class::cast)
-                        .map(StringAttributeValue::getValue)
-                        .collect(Collectors.toUnmodifiableList());
-            }
-            
-            log.warn("{} No source attribute {} available to produce audience claim", getLogPrefix(),
-                    audienceAttribute);
-            return null;
-        }
-        
-        if (tokenRequest.getResources() != null && !tokenRequest.getResources().isEmpty()) {
-            log.debug("{} Using originally requested resource(s) as audience", getLogPrefix());
-            return tokenRequest.getResources().stream().map(URI::toString).collect(Collectors.toUnmodifiableList());
-        }
-        
-        return null;
-    }
 
-}
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/EncryptAccessToken.java
similarity index 71%
copy from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
copy to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/EncryptAccessToken.java
index a1ba2a4a..72949c67 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/EncryptAccessToken.java
@@ -17,47 +17,48 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
 
+import java.text.ParseException;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.AccessTokenContext;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
-import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction;
-
-import org.opensaml.messaging.context.navigate.ChildContextLookup;
-import org.opensaml.profile.action.ActionSupport;
-
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
- * Action that creates a Access Token, and sets it to work context
- * {@link OIDCAuthenticationResponseContext#getAccessToken()} located under
- * {@link ProfileRequestContext#getOutboundMessageContext()}.
+ * Action that handles JWT access token encryption.
  */
-public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction {
+public class EncryptAccessToken extends AbstractEncryptTokenAction {
 
     /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(SetAccessTokenToResponseContext.class);
+    @Nonnull private Logger log = LoggerFactory.getLogger(EncryptAccessToken.class);
 
     /** Strategy used to locate the subcontext with the token. */
     @Nonnull private Function<ProfileRequestContext,AccessTokenContext> accessTokenContextLookupStrategy;
     
     /** Token context. */
     @Nullable private AccessTokenContext tokenCtx; 
-    
+
     /** Constructor. */
-    public SetAccessTokenToResponseContext() {
-        // PRC -> inbound message context -> OIDC response context -> ATC
-        accessTokenContextLookupStrategy = new ChildContextLookup<>(AccessTokenContext.class, true).compose(
+    public EncryptAccessToken() {
+        // PRC -> outbound message context -> OIDC response context -> ATC
+        accessTokenContextLookupStrategy = new ChildContextLookup<>(AccessTokenContext.class).compose(
                 new ChildContextLookup<>(OIDCAuthenticationResponseContext.class).compose(
                         new OutboundMessageContextLookup()));
     }
@@ -72,9 +73,9 @@ public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
         accessTokenContextLookupStrategy =
-                Constraint.isNotNull(strategy, "AccessTokenContext creation strategy cannot be null");
+                Constraint.isNotNull(strategy, "AccessTokenContext lookup strategy cannot be null");
     }
-    
+
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -90,27 +91,31 @@ public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction
             return false;
         }
         
-        if (tokenCtx.getJWT() == null && tokenCtx.getOpaque() == null) {
-            log.debug("{} Access token is missing", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
-            return false;
-        }
-        
         return true;
     }
-
+    
     /** {@inheritDoc} */
     @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        
-        final String token;
-        if (tokenCtx.getJWT() != null) {
-            token = tokenCtx.getJWT().serialize();
-        } else {
-            token = tokenCtx.getOpaque();
+    protected Payload getPayload(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (tokenCtx.getJWT() instanceof SignedJWT) {
+            return new Payload((SignedJWT) tokenCtx.getJWT());
+        } else if (tokenCtx.getJWT() instanceof PlainJWT) {
+            try {
+                return new Payload(tokenCtx.getJWT().getJWTClaimsSet().getClaims());
+            } catch (final ParseException e) {
+                log.error("{} Unable to convert plaintext JWT to claims set", getLogPrefix(), e);
+            }
         }
-        
-        getOidcResponseContext().setAccessToken(token, tokenCtx.getLifetime(), getOidcResponseContext().getScope());
+         
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void setProcessedToken(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final EncryptedJWT token) {
+        tokenCtx.setJWT(token);
     }
 
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/EnforceAudienceConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/EnforceAudienceConfiguration.java
new file mode 100644
index 00000000..70ab9f43
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/EnforceAudienceConfiguration.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.plugin.oidc.op.oauth2.profile.impl;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.oidc.profile.config.logic.AllowUntrustedAudiencePredicate;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Profile action that evaluates the resolved configuration settings for the audience of a
+ * token request.
+ * 
+ * <p>This action recognizes the duality of a token request whereby the original client and
+ * the primary recipient are both relying parties of the system at different stages of the
+ * flow. Either or both of them may be unverified, but enforcement of this for the audience
+ * depends on a profile setting applied to the client's original profile configuration.</p>
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CTX}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ * 
+ * @since 3.1.0
+ */
+public class EnforceAudienceConfiguration extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(EnforceAudienceConfiguration.class);
+
+    /** Strategy that will return the requester's {@link RelyingPartyContext}. */
+    @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> requesterContextLookupStrategy;
+
+    /** Strategy that will return the audience's {@link RelyingPartyContext}. */
+    @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> audienceContextLookupStrategy;
+
+    /** Predicate that determines whether to allow an unverified audience. */
+    @Nonnull private Predicate<ProfileRequestContext> allowUntrustedAudienceCondition;
+    
+    /** Original context. */
+    @Nullable private RelyingPartyContext reqContext;
+
+    /** New context. */
+    @Nullable private RelyingPartyContext audContext;
+
+    /** Constructor. */
+    public EnforceAudienceConfiguration() {
+        requesterContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+        audienceContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class).compose(
+                new OutboundMessageContextLookup());
+        allowUntrustedAudienceCondition = new AllowUntrustedAudiencePredicate();
+    }
+    
+    /**
+     * Set the strategy used to return the original requester's {@link RelyingPartyContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRequesterContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        requesterContextLookupStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to return the audience's {@link RelyingPartyContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAudienceContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        audienceContextLookupStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the condition used to establish whether to allow an unverified audience.
+     * 
+     * @param condition condition to set
+     */
+    public void setUntrustedAudienceCondition(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        allowUntrustedAudienceCondition = Constraint.isNotNull(condition,
+                "Untrusted audience condition cannot be null");
+    }
+
+    /** {@inheritDoc}. */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        reqContext = requesterContextLookupStrategy.apply(profileRequestContext);
+        audContext = audienceContextLookupStrategy.apply(profileRequestContext);
+        if (reqContext == null ||audContext == null) {
+            log.error("{} Requester or audience RelyingPartyContext missing", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+            return false;
+        }
+        
+        return true;
+    }
+
+    /** {@inheritDoc}. */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (audContext.isVerified() && audContext.getProfileConfig() == null) {
+            log.warn("{} Token profile not enabled for verified audience: {}", getLogPrefix(),
+                    audContext.getRelyingPartyId());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return;
+        }
+        
+        if (!audContext.isVerified()) {
+            final boolean allow = allowUntrustedAudienceCondition.test(profileRequestContext);
+            if (!allow) {
+                log.warn("{} Client {} not permitted to request access token for untrusted audience {}", getLogPrefix(),
+                        reqContext.getRelyingPartyId(), audContext.getRelyingPartyId());
+                ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+                return;
+            }
+            
+            if (audContext.getProfileConfig() == null) {
+                log.debug("{} Applying previously resolved Token profile configuration to untrusted audience",
+                        getLogPrefix());
+                audContext.setProfileConfig(reqContext.getProfileConfig());
+            }
+        }
+        
+        log.debug("{} Authorized token issuance from {} client ({}) to {} audience ({})", getLogPrefix(),
+                reqContext.isVerified() ? "verified" : "unverified", reqContext.getRelyingPartyId(),
+                        audContext.isVerified() ? "verified" : "unverified", audContext.getRelyingPartyId());
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/OAuthAttributeResolutionContextDecorator.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/OAuthAttributeResolutionContextDecorator.java
deleted file mode 100644
index b031350f..00000000
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/OAuthAttributeResolutionContextDecorator.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements.  See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You 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.idp.plugin.oidc.op.oauth2.profile.impl;
-
-import java.net.URI;
-import java.util.Collection;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-import javax.annotation.Nullable;
-
-import org.opensaml.messaging.context.MessageContext;
-import org.opensaml.profile.context.ProfileRequestContext;
-
-import com.nimbusds.oauth2.sdk.TokenRequest;
-
-import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
-import net.shibboleth.idp.plugin.oidc.op.profile.context.OAuthAttributeResolutionContext;
-
-/**
- * Adds OAuth-specific details about the resolution request to the tree.
- * 
- * @since 3.1.0
- */
-public class OAuthAttributeResolutionContextDecorator implements Consumer<AttributeResolutionContext> {
-
-    /** {@inheritDoc} */
-    @Override
-    public void accept(@Nullable final AttributeResolutionContext input) {
-        
-        final OAuthAttributeResolutionContext oauthCtx =
-                input.getSubcontext(OAuthAttributeResolutionContext.class, true);
-        
-        if (input != null && input.getParent() instanceof ProfileRequestContext) {
-            final MessageContext inbound = ((ProfileRequestContext) input.getParent()).getInboundMessageContext();
-            if (inbound != null && inbound.getMessage() instanceof TokenRequest) {
-                final Collection<URI> resources = ((TokenRequest) inbound.getMessage()).getResources();
-                if (resources != null) {
-                    oauthCtx.getResources().addAll(
-                            resources.stream().map(URI::toString).collect(Collectors.toUnmodifiableList()));
-                }
-            }
-            
-            final MessageContext outbound = ((ProfileRequestContext) input.getParent()).getOutboundMessageContext();
-            if (outbound != null) {
-                final OIDCAuthenticationResponseContext responseCtx =
-                        outbound.getSubcontext(OIDCAuthenticationResponseContext.class);
-                if (responseCtx != null && responseCtx.getScope() != null) {
-                    oauthCtx.getScope().addAll(responseCtx.getScope().toStringList());
-                }
-            }
-        }
-    }
-
-}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
index a1ba2a4a..16c590cf 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
@@ -56,23 +56,23 @@ public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction
     
     /** Constructor. */
     public SetAccessTokenToResponseContext() {
-        // PRC -> inbound message context -> OIDC response context -> ATC
-        accessTokenContextLookupStrategy = new ChildContextLookup<>(AccessTokenContext.class, true).compose(
+        // PRC -> outbound message context -> OIDC response context -> ATC
+        accessTokenContextLookupStrategy = new ChildContextLookup<>(AccessTokenContext.class).compose(
                 new ChildContextLookup<>(OIDCAuthenticationResponseContext.class).compose(
                         new OutboundMessageContextLookup()));
     }
 
     /**
-     * Set the strategy used to create the {@link AccessTokenContext} to use.
+     * Set the strategy used to lookup the {@link AccessTokenContext} to use.
      * 
-     * @param strategy creation strategy
+     * @param strategy lookup strategy
      */
-    public void setAccessTokenContextCreationStrategy(
+    public void setAccessTokenContextLookupStrategy(
             @Nonnull final Function<ProfileRequestContext,AccessTokenContext> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
         accessTokenContextLookupStrategy =
-                Constraint.isNotNull(strategy, "AccessTokenContext creation strategy cannot be null");
+                Constraint.isNotNull(strategy, "AccessTokenContext lookup strategy cannot be null");
     }
     
     /** {@inheritDoc} */
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudience.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudience.java
new file mode 100644
index 00000000..c73bb8f5
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudience.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.plugin.oidc.op.oauth2.profile.impl;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseTokenClaimsContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ClientInfoAudienceLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestAudienceLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCAuthenticationResponseAction;
+import net.shibboleth.idp.profile.context.navigate.RelyingPartyIdLookupFunction;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Action that validates requested resource/audience values against a computed set of "allowed"
+ * values.
+ * 
+ * <p>This is an ordered list, so the first allowed value determines the "primary" audience of
+ * the eventual token.</p>
+ * 
+ * <p>Requesting values is optional, in the absence of which at least one allowed value must exist
+ * and be returned, or a failure event will be signaled.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#ACCESS_DENIED}
+ */
+public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ValidateAudience.class);
+
+    /** Strategy used to obtain the relying party ID. */
+    @Nonnull private Function<ProfileRequestContext,String> relyingPartyIdLookupStrategy;
+    
+    /** Strategy used to obtain the requested audience. */
+    @Nullable private Function<ProfileRequestContext,List<String>> requestedAudienceLookupStrategy;
+
+    /** Strategy used to obtain the audience allowed for the client. */
+    @Nonnull private Function<ProfileRequestContext,List<String>> allowedAudienceLookupStrategy;
+
+    /** Strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext}. */
+    @Nonnull
+    private Function<ProfileRequestContext,OIDCAuthenticationResponseTokenClaimsContext>
+    tokenClaimsContextLookupStrategy;
+    
+    /** Constructor. */
+    public ValidateAudience() {
+        requestedAudienceLookupStrategy = new TokenRequestAudienceLookupFunction();
+        relyingPartyIdLookupStrategy = new RelyingPartyIdLookupFunction();
+        allowedAudienceLookupStrategy = new ClientInfoAudienceLookupFunction().compose(
+                new DefaultOIDCMetadataContextLookupFunction());
+    }
+
+    /**
+     * Set the strategy used to obtain the relying party ID.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRelyingPartyIdLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        relyingPartyIdLookupStrategy = Constraint.isNotNull(strategy,
+                "Relying party ID lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to locate the requested audience to validate.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRequestedAudienceLookupStrategy(
+            @Nullable final Function<ProfileRequestContext,List<String>> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        requestedAudienceLookupStrategy = strategy;
+    }
+
+
+    /**
+     * Set the strategy used to locate the allowed audience for the client.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAllowedAudienceLookupStrategy(@Nonnull final Function<ProfileRequestContext,List<String>> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        allowedAudienceLookupStrategy = Constraint.isNotNull(strategy,
+                "Allowed scope lookyp strategy cannot be null");
+    }
+    
+// Checkstyle: CyclomaticComplexity OFF
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        final String clientId = relyingPartyIdLookupStrategy.apply(profileRequestContext);
+        
+        // These may come from metadata or be supplemented or substituted from elsewhere.
+        final List<String> allowedAudience = allowedAudienceLookupStrategy.apply(profileRequestContext);
+        if (allowedAudience == null || allowedAudience.isEmpty()) {
+            log.warn("{} No allowed audience for client {}", getLogPrefix(), clientId);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
+            return;
+        }
+        
+        // These come from a previous authorization grant (authz code or refresh token).
+        List<String> previouslyGrantedAudience = null;
+        if (getOidcResponseContext().getAuthorizationGrantClaimsSet() != null) {
+            previouslyGrantedAudience = getOidcResponseContext().getAuthorizationGrantClaimsSet().getAudience();
+        }
+
+        // These come from a request object or parameter. Absent by definition on the UserInfo endpoint.
+        List<String> requestedAudience = requestedAudienceLookupStrategy != null ?
+                requestedAudienceLookupStrategy.apply(profileRequestContext) : null;
+        
+        if (requestedAudience == null) {
+            // With none requested, simply swap requested for previously granted, if any.
+            // Set previous set to null since there's no need to filter against it.
+            requestedAudience = previouslyGrantedAudience;
+            previouslyGrantedAudience = null;
+        }
+        
+        if (requestedAudience == null) {
+            // Nothing requested or previously granted, so take the first allowed value. 
+            log.debug("{} No audience in request for client {}, using first allowed", getLogPrefix(), clientId);
+            requestedAudience = Collections.singletonList(allowedAudience.get(0));
+        }
+        
+        final List<String> effectiveAudience = new ArrayList<>();
+        
+        for (final String aud : requestedAudience) {
+            if (!allowedAudience.contains(aud)) {
+                log.warn("{} Omitting requested but unregistered audience {} for RP {}", getLogPrefix(), aud,
+                        clientId);
+            } else if (previouslyGrantedAudience != null && !previouslyGrantedAudience.contains(aud)) {
+                log.warn("{} Omitting requested but previously ungranted audience {} for RP {}", getLogPrefix(),
+                        aud, clientId);
+            } else {
+                effectiveAudience.add(aud);
+            }
+        }
+        
+        if (effectiveAudience.isEmpty()) {
+            log.warn("{} No allowed audience for client {}", getLogPrefix(), clientId);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
+        } else {
+            log.debug("{} Computed audience for client {}: {}", getLogPrefix(), clientId, effectiveAudience);
+            getOidcResponseContext().getAudience().addAll(effectiveAudience);
+        }
+    }
+// Checkstyle: CyclomaticComplexity ON
+    
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCAuthenticationResponseAction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCAuthenticationResponseAction.java
index 69fbf388..33703ce6 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCAuthenticationResponseAction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCAuthenticationResponseAction.java
@@ -37,7 +37,7 @@ import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
  * {@link ProfileRequestContext#getOutboundMessageContext()}. Extends baseclass that offers actions on
  * {@link AuthenticationRequest} found via {@link MessageContext#getMessage()}.
  */
-abstract class AbstractOIDCAuthenticationResponseAction extends AbstractOIDCAuthenticationRequestAction {
+public abstract class AbstractOIDCAuthenticationResponseAction extends AbstractOIDCAuthenticationRequestAction {
 
     /** Class logger. */
     @Nonnull
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/EncryptProcessedToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/EncryptProcessedToken.java
index 83cd30a3..5a82b9b9 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/EncryptProcessedToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/EncryptProcessedToken.java
@@ -17,147 +17,46 @@
 
 package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 
-import java.security.interfaces.ECPublicKey;
-import java.security.interfaces.RSAPublicKey;
-import java.text.ParseException;
-import java.util.function.Function;
-
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 
-import org.opensaml.messaging.context.navigate.ChildContextLookup;
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.saml.saml2.profile.context.EncryptionContext;
-import org.opensaml.security.credential.Credential;
-import org.opensaml.xmlsec.EncryptionParameters;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-import com.nimbusds.jose.EncryptionMethod;
-import com.nimbusds.jose.JOSEException;
-import com.nimbusds.jose.JWEAlgorithm;
-import com.nimbusds.jose.JWEHeader;
-import com.nimbusds.jose.JWEObject;
 import com.nimbusds.jose.Payload;
-import com.nimbusds.jose.crypto.AESEncrypter;
-import com.nimbusds.jose.crypto.ECDHEncrypter;
-import com.nimbusds.jose.crypto.RSAEncrypter;
 import com.nimbusds.jwt.EncryptedJWT;
 import com.nimbusds.jwt.SignedJWT;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
-import net.shibboleth.idp.profile.context.RelyingPartyContext;
-import net.shibboleth.oidc.security.impl.CredentialConversionUtil;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.AbstractEncryptTokenAction;
 
 /**
- * Action that serves both id token and user info response encryption. Existence of encryption parameters is taken as
- * indication whether the encryption should take place. Action assumes the content to be encrypted is located primarily
+ * Action that serves both id token and user info response encryption.
+ * 
+ * <p>Action assumes the content to be encrypted is located primarily
  * by {@link OIDCAuthenticationResponseContext#getProcessedToken()} returning either signed id token or signed user info
  * response. If such information is not available action assumes the data to be encrypted is
  * {@link OIDCAuthenticationResponseContext#getUserInfo()} containing bare user info response. If neither of the sources
- * for encryption exists the actions fails.
+ * for encryption exists the actions fails.</p>
  */
-public class EncryptProcessedToken extends AbstractOIDCResponseAction {
-
-    /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(EncryptProcessedToken.class);
-
-    /** Strategy used to look up the {@link EncryptionContext} to store parameters in. */
-    @Nonnull private Function<ProfileRequestContext, EncryptionContext> encryptionContextLookupStrategy;
-
-    /** Encryption parameters for encrypting payload. */
-    @Nullable private EncryptionParameters params;
-
-    /** Payload to encrypt. */
-    @Nullable private Payload payload;
-
-    /**
-     * Constructor.
-     */
-    public EncryptProcessedToken() {
-        encryptionContextLookupStrategy = new ChildContextLookup<>(EncryptionContext.class).compose(
-                new ChildContextLookup<>(RelyingPartyContext.class));
-    }
-
-    /**
-     * Set the strategy used to look up the {@link EncryptionContext} to set the flags for.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setEncryptionContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, EncryptionContext> strategy) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
-        encryptionContextLookupStrategy =
-                Constraint.isNotNull(strategy, "EncryptionContext lookup strategy cannot be null");
-    }
+public class EncryptProcessedToken extends AbstractEncryptTokenAction {
 
     /** {@inheritDoc} */
     @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        if (!super.doPreExecute(profileRequestContext)) {
-            return false;
-        }
+    protected Payload getPayload(@Nonnull final ProfileRequestContext profileRequestContext) {
         
-        final EncryptionContext encryptCtx = encryptionContextLookupStrategy.apply(profileRequestContext);
-        if (encryptCtx == null) {
-            log.error("{} No EncryptionContext returned by lookup strategy", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        }
-        params = encryptCtx.getAssertionEncryptionParameters();
-        if (params == null) {
-            log.debug("{} No Encryption parameters, nothing to do", getLogPrefix());
-            return false;
-        }
         if (getOidcResponseContext().getProcessedToken() != null) {
-            payload = new Payload((SignedJWT) getOidcResponseContext().getProcessedToken());
+            return new Payload((SignedJWT) getOidcResponseContext().getProcessedToken());
         } else if (getOidcResponseContext().getUserInfo() != null) {
-            payload = new Payload(getOidcResponseContext().getUserInfo().toJSONObject());
+            return new Payload(getOidcResponseContext().getUserInfo().toJSONObject());
+        } else {
+            return null;
         }
-        if (payload == null) {
-            log.error("{} Instructed to encrypt but no plain text source available", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
-            return false;
-        }
-        
-        return true;
     }
 
     /** {@inheritDoc} */
     @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-
-        final JWEAlgorithm encAlg = JWEAlgorithm.parse(params.getKeyTransportEncryptionAlgorithm());
-        final Credential credential = params.getKeyTransportEncryptionCredential();
-        final EncryptionMethod encEnc = EncryptionMethod.parse(params.getDataEncryptionAlgorithm());
-        final String kid = CredentialConversionUtil.resolveKid(credential);
-
-        log.debug("{} encrypting with key {} and params alg: {} enc: {}", getLogPrefix(), kid, encAlg.getName(),
-                encEnc.getName());
-
-        final JWEObject jweObject =
-                new JWEObject(new JWEHeader.Builder(encAlg, encEnc).contentType("JWT").keyID(kid).build(), payload);
-        try {
-            if (JWEAlgorithm.Family.RSA.contains(encAlg)) {
-                jweObject.encrypt(new RSAEncrypter((RSAPublicKey) credential.getPublicKey()));
-            } else if (JWEAlgorithm.Family.ECDH_ES.contains(encAlg)) {
-                jweObject.encrypt(new ECDHEncrypter((ECPublicKey) credential.getPublicKey()));
-            } else if (JWEAlgorithm.Family.SYMMETRIC.contains(encAlg)) {
-                jweObject.encrypt(new AESEncrypter(credential.getSecretKey()));
-            } else {
-                log.error("{} Unsupported algorithm {}", getLogPrefix(), encAlg.getName());
-                ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
-            }
-            getOidcResponseContext().setProcessedToken(EncryptedJWT.parse(jweObject.serialize()));
-        } catch (final JOSEException | ParseException e) {
-            log.error("{} Encryption failed {}", getLogPrefix(), e.getMessage());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
-        }
+    protected void setProcessedToken(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final EncryptedJWT token) {
+        getOidcResponseContext().setProcessedToken(token);
     }
 
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/InitializeRelyingPartyContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/InitializeRelyingPartyContext.java
index a2e21376..b92d5722 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/InitializeRelyingPartyContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/InitializeRelyingPartyContext.java
@@ -25,10 +25,10 @@ import javax.annotation.Nullable;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
 import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultClientIDLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction;
 import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.idp.profile.IdPEventIds;
 import net.shibboleth.idp.profile.context.RelyingPartyContext;
@@ -67,13 +67,16 @@ public class InitializeRelyingPartyContext extends AbstractProfileAction {
 
     /** OIDC client id. */
     @Nullable private ClientID clientId;
+    
+    /** Use the inbound message tree? */
+    private boolean inbound;
 
     /** Constructor. */
     public InitializeRelyingPartyContext() {
         relyingPartyContextCreationStrategy = new ChildContextLookup<>(RelyingPartyContext.class, true);
-        oidcMetadataContextLookupStrategy = new ChildContextLookup<>(OIDCMetadataContext.class).compose(
-                new InboundMessageContextLookup());
+        oidcMetadataContextLookupStrategy = new DefaultOIDCMetadataContextLookupFunction();
         clientIDLookupStrategy = new DefaultClientIDLookupFunction();
+        inbound = true;
     }
 
     /**
@@ -113,16 +116,32 @@ public class InitializeRelyingPartyContext extends AbstractProfileAction {
                 Constraint.isNotNull(strategy, "OIDCMetadataContext lookup strategy cannot be null");
     }
 
+    /**
+     * Sets which message tree to use in deriving the ClientID and OIDC metadata.
+     * 
+     * <p>Defaults to true (the inbound tree).</p>
+     * 
+     * @param flag true iff the inbound tree should be used
+     * 
+     * @since 3.1.0
+     */
+    public void setInbound(final boolean flag) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        inbound = flag;
+    }
+    
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         if (!super.doPreExecute(profileRequestContext)) {
-            log.error("{} pre-execute failed", getLogPrefix());
             return false;
         }
-        clientId = clientIDLookupStrategy.apply(profileRequestContext.getInboundMessageContext());
+        
+        clientId = clientIDLookupStrategy.apply(inbound ? profileRequestContext.getInboundMessageContext() :
+            profileRequestContext.getOutboundMessageContext());
         if (clientId == null) {
-            log.error("{} Unable to locate client id from the request", getLogPrefix());
+            log.error("{} Unable to obtain client ID", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
             return false;
         }
@@ -139,8 +158,10 @@ public class InitializeRelyingPartyContext extends AbstractProfileAction {
             ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
             return;
         }
-        log.debug("Attaching RelyingPartyContext for rp {}", clientId.getValue());
+        
+        log.debug("Attaching RelyingPartyContext for {}", clientId.getValue());
         rpContext.setRelyingPartyId(clientId.getValue());
+        
         final OIDCMetadataContext oidcContext = oidcMetadataContextLookupStrategy.apply(profileRequestContext);
         if (oidcContext != null && oidcContext.getClientInformation() != null
                 && clientId.equals(oidcContext.getClientInformation().getID())) {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/OIDCMetadataLookupHandler.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/OIDCMetadataLookupHandler.java
index c75e4ee0..0fb31333 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/OIDCMetadataLookupHandler.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/OIDCMetadataLookupHandler.java
@@ -99,11 +99,14 @@ public class OIDCMetadataLookupHandler extends AbstractMessageHandler {
     @Override
     protected void doInvoke(@Nonnull final MessageContext messageContext) throws MessageHandlerException {
         ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
-        // Resolve client id from inbound message
+        
         final ClientID clientId = clientIDLookupStrategy.apply(messageContext);
-        // Resolve metadata for client id
-        final ClientIDCriterion clientCriterion = new ClientIDCriterion(clientId);
-        final CriteriaSet criteria = new CriteriaSet(clientCriterion);
+        if (clientId == null) {
+            log.debug("{} No client ID available", getLogPrefix());
+            return;
+        }
+        
+        final CriteriaSet criteria = new CriteriaSet(new ClientIDCriterion(clientId));
         try {
             final OIDCClientInformation clientInformation = clientResolver.resolveSingle(criteria);
             if (clientInformation == null) {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCEncryptionParameters.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCEncryptionParameters.java
index 7166ae90..7aa18f6a 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCEncryptionParameters.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCEncryptionParameters.java
@@ -20,6 +20,7 @@ package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -40,6 +41,7 @@ import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
 import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction;
 import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.oidc.profile.config.logic.EncryptionOptionalPredicate;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
@@ -76,9 +78,12 @@ public class PopulateOIDCEncryptionParameters extends AbstractProfileAction {
 
     /** Whether we resolve encryption or decryption parameters. */
     private boolean forDecryption;
+    
+    /** Predicate to determine how to proceed if no encryption parameters are resolved. */
+    @Nonnull private Predicate<ProfileRequestContext> encryptionOptionalPredicate;
 
     /** Strategy used to look up the {@link EncryptionContext} to store parameters in. */
-    @Nonnull private Function<ProfileRequestContext, EncryptionContext> encryptionContextLookupStrategy;
+    @Nonnull private Function<ProfileRequestContext,EncryptionContext> encryptionContextLookupStrategy;
 
     /** Strategy used to look up a per-request {@link EncryptionConfiguration} list. */
     @NonnullAfterInit private Function<ProfileRequestContext,List<EncryptionConfiguration>> configurationLookupStrategy;
@@ -90,7 +95,13 @@ public class PopulateOIDCEncryptionParameters extends AbstractProfileAction {
     @Nullable @NonnullElements private List<EncryptionConfiguration> encryptionConfigurations;
 
     /** Strategy used to look up a OIDC metadata context. */
-    @Nullable private Function<ProfileRequestContext, OIDCMetadataContext> oidcMetadataContextLookupStrategy;
+    @Nullable private Function<ProfileRequestContext,OIDCMetadataContext> oidcMetadataContextLookupStrategy;
+    
+    /** Whether encryption is optional. */
+    private boolean encryptionOptional;
+    
+    /** Context to populate. */
+    private EncryptionContext encryptionContext;
 
     /** Constructor. */
     public PopulateOIDCEncryptionParameters() {
@@ -98,8 +109,9 @@ public class PopulateOIDCEncryptionParameters extends AbstractProfileAction {
         oidcMetadataContextLookupStrategy = new DefaultOIDCMetadataContextLookupFunction();
         encryptionContextLookupStrategy = new ChildContextLookup<>(EncryptionContext.class, true).compose(
                 new ChildContextLookup<>(RelyingPartyContext.class));
+        encryptionOptionalPredicate = new EncryptionOptionalPredicate();
     }
-
+    
     /**
      * Whether we resolve encryption or decryption parameters.
      * 
@@ -107,8 +119,22 @@ public class PopulateOIDCEncryptionParameters extends AbstractProfileAction {
      */
     public void setForDecryption(final boolean flag) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
         forDecryption = flag;
     }
+   
+    /**
+     * Sets the condition to apply to determine how to proceed if encryption parameter resolution fails.
+     * 
+     * @param condition condition to set
+     * 
+     * @since 3.1.0
+     */
+    public void setEncryptionOptionalPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        encryptionOptionalPredicate = Constraint.isNotNull(condition, "Condition cannot be null");
+    }
 
     /**
      * Set the strategy used to look up the {@link EncryptionContext} to set the flags for.
@@ -129,7 +155,7 @@ public class PopulateOIDCEncryptionParameters extends AbstractProfileAction {
      * 
      * @param strategy lookup strategy
      */
-    public void setOIDCMetadataContextContextLookupStrategy(
+    public void setOidcMetadataContextLookupStrategy(
             @Nonnull final Function<ProfileRequestContext, OIDCMetadataContext> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
 
@@ -177,20 +203,35 @@ public class PopulateOIDCEncryptionParameters extends AbstractProfileAction {
         }
     }
 
-    // Checkstyle: CyclomaticComplexity OFF
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        encryptionContext = encryptionContextLookupStrategy.apply(profileRequestContext);
+        if (encryptionContext == null) {
+            log.debug("{} No EncryptionContext returned by lookup strategy", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        if (!forDecryption) {
+            encryptionOptional = encryptionOptionalPredicate.test(profileRequestContext);
+        }
+        
+        return true;
+    }
 
+// Checkstyle: CyclomaticComplexity OFF
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
-        log.debug("{} Resolving EncryptionParameters for request, purpose {}", getLogPrefix(),
+        log.debug("{} Resolving EncryptionParameters for {}", getLogPrefix(),
                 forDecryption ? "request object decryption" : "response encryption");
-        final EncryptionContext encryptCtx = encryptionContextLookupStrategy.apply(profileRequestContext);
-        if (encryptCtx == null) {
-            log.debug("{} No EncryptionContext returned by lookup strategy", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return;
-        }
+        
         try {
             encryptionConfigurations = configurationLookupStrategy.apply(profileRequestContext);
             if (encryptionConfigurations == null || encryptionConfigurations.isEmpty()) {
@@ -198,34 +239,37 @@ public class PopulateOIDCEncryptionParameters extends AbstractProfileAction {
             }
             final CriteriaSet criteria = buildCriteriaSet(profileRequestContext);
             final EncryptionParameters params = encParamsresolver.resolveSingle(criteria);
-            log.debug("{} {} EncryptionParameters for {}", getLogPrefix(),
-                    params != null ? "Resolved" : "Failed to resolve",
-                    forDecryption ? "request object decryption" : "response encryption");
+            
             if (params != null) {
+                log.debug("{} Resolved EncryptionParameters for {}", getLogPrefix(),
+                        forDecryption ? "request object decryption" : "response encryption");
                 if (forDecryption) {
                     // Decryption parameters for request object decryption
-                    encryptCtx.setAttributeEncryptionParameters(params);
+                    encryptionContext.setAttributeEncryptionParameters(params);
                 } else {
                     // Indicates that id token or userinfo response should be encrypted
-                    encryptCtx.setAssertionEncryptionParameters(params);
+                    encryptionContext.setAssertionEncryptionParameters(params);
                 }
-                return;
-            }
-            final EncryptionOptionalCriterion encryptionOptionalCrit = criteria.get(EncryptionOptionalCriterion.class);
-            if (encryptionOptionalCrit != null) {
-                if (encryptionOptionalCrit.isEncryptionOptional()) {
-                    log.debug("{} Encryption optional", getLogPrefix());
-                    return;
+            } else {
+                if (encryptionOptional) {
+                    log.debug("{} Resolver returned no EncryptionParameters", getLogPrefix());
+                    log.debug("{} Encryption is optional, ignoring inability to encrypt", getLogPrefix());
+                } else {
+                    log.warn("{} Resolver returned no EncryptionParameters", getLogPrefix());
+                    ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_SEC_CFG);
                 }
             }
         } catch (final ResolverException e) {
             log.error("{} Error resolving EncryptionParameters", getLogPrefix(), e);
+            if (encryptionOptional) {
+                log.debug("{} Encryption is optional, ignoring inability to encrypt", getLogPrefix());
+            } else {
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_SEC_CFG);
+            }
         }
-        ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_SEC_CFG);
     }
+// Checkstyle: CyclomaticComplexity ON
     
-    // Checkstyle: CyclomaticComplexity ON
-
     /**
      * Build the criteria used as input to the {@link EncryptionParametersResolver}.
      * 
@@ -237,14 +281,19 @@ public class PopulateOIDCEncryptionParameters extends AbstractProfileAction {
     private CriteriaSet buildCriteriaSet(@Nonnull final ProfileRequestContext profileRequestContext) {
 
         final CriteriaSet criteria = new CriteriaSet(new EncryptionConfigurationCriterion(encryptionConfigurations));
+        
+        if (!forDecryption) {
+            criteria.add(new EncryptionOptionalCriterion(encryptionOptional));
+        }
+        
         final OIDCMetadataContext oidcMetadataCtx = oidcMetadataContextLookupStrategy.apply(profileRequestContext);
         if (oidcMetadataCtx != null && oidcMetadataCtx.getClientInformation() != null) {
             log.debug(
-                    "{} Adding oidc client information to resolution criteria for key transport/encryption algorithms",
+                    "{} Adding OIDC client information to resolution criteria for encryption algorithms",
                     getLogPrefix());
             criteria.add(new ClientInformationCriterion(oidcMetadataCtx.getClientInformation()));
         } else {
-            log.debug("{} oidcMetadataCtx is null", getLogPrefix());
+            log.debug("{} No OIDC client information available", getLogPrefix());
         }
         return criteria;
     }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCSignatureSigningParameters.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCSignatureSigningParameters.java
index 284ced12..4f84a09b 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCSignatureSigningParameters.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCSignatureSigningParameters.java
@@ -28,6 +28,7 @@ import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.AbstractHandlerDelegatingProfileAction;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
 import org.opensaml.saml.common.messaging.context.SAMLMetadataContext;
 import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
@@ -94,7 +95,7 @@ public class PopulateOIDCSignatureSigningParameters
         // Default: outbound msg context -> SAMLPeerEntityContext -> SAMLMetadataContext
         metadataContextLookupStrategy = new ChildContextLookup<>(SAMLMetadataContext.class).compose(
                 new ChildContextLookup<>(SAMLPeerEntityContext.class).compose(
-                        new OutboundMessageContextLookup()));
+                        new InboundMessageContextLookup()));
 
         setErrorEvent(EventIds.INVALID_SEC_CFG);
     }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScope.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScope.java
index a048ca54..ae30bf7a 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScope.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScope.java
@@ -69,10 +69,10 @@ public class ValidateScope extends AbstractOIDCAuthenticationResponseAction {
     @Nonnull private Function<ProfileRequestContext,String> relyingPartyIdLookupStrategy;
     
     /** Strategy used to obtain the requested scope value. */
-    @Nullable private Function<ProfileRequestContext,Scope> requestedScopesLookupStrategy;
+    @Nullable private Function<ProfileRequestContext,Scope> requestedScopeLookupStrategy;
 
     /** Strategy used to obtain the scope allowed for the client. */
-    @Nonnull private Function<ProfileRequestContext,Scope> allowedScopesLookupStrategy;
+    @Nonnull private Function<ProfileRequestContext,Scope> allowedScopeLookupStrategy;
 
     /** Strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext}. */
     @Nonnull
@@ -81,9 +81,9 @@ public class ValidateScope extends AbstractOIDCAuthenticationResponseAction {
     
     /** Constructor. */
     public ValidateScope() {
-        requestedScopesLookupStrategy = new DefaultRequestedScopeLookupFunction();
+        requestedScopeLookupStrategy = new DefaultRequestedScopeLookupFunction();
         relyingPartyIdLookupStrategy = new RelyingPartyIdLookupFunction();
-        allowedScopesLookupStrategy = new ClientInfoScopeLookupFunction().compose(
+        allowedScopeLookupStrategy = new ClientInfoScopeLookupFunction().compose(
                 new DefaultOIDCMetadataContextLookupFunction());
         tokenClaimsContextLookupStrategy =
                 new ChildContextLookup<>(OIDCAuthenticationResponseTokenClaimsContext.class).compose(
@@ -107,10 +107,10 @@ public class ValidateScope extends AbstractOIDCAuthenticationResponseAction {
      * 
      * @param strategy lookup strategy
      */
-    public void setRequestedScopesLookupStrategy(@Nullable final Function<ProfileRequestContext,Scope> strategy) {
+    public void setRequestedScopeLookupStrategy(@Nullable final Function<ProfileRequestContext,Scope> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
-        requestedScopesLookupStrategy = strategy;
+        requestedScopeLookupStrategy = strategy;
     }
 
 
@@ -119,10 +119,10 @@ public class ValidateScope extends AbstractOIDCAuthenticationResponseAction {
      * 
      * @param strategy lookup strategy
      */
-    public void setAllowedScopesLookupStrategy(@Nonnull final Function<ProfileRequestContext,Scope> strategy) {
+    public void setAllowedScopeLookupStrategy(@Nonnull final Function<ProfileRequestContext,Scope> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
-        allowedScopesLookupStrategy = Constraint.isNotNull(strategy,
+        allowedScopeLookupStrategy = Constraint.isNotNull(strategy,
                 "Allowed scope lookyp strategy cannot be null");
     }
     
@@ -149,7 +149,7 @@ public class ValidateScope extends AbstractOIDCAuthenticationResponseAction {
         final String clientId = relyingPartyIdLookupStrategy.apply(profileRequestContext);
         
         // These typically come from metadata but may be supplemented or substituted from elsewhere.
-        final Scope allowedScopes = allowedScopesLookupStrategy.apply(profileRequestContext);
+        final Scope allowedScopes = allowedScopeLookupStrategy.apply(profileRequestContext);
         if (allowedScopes == null || allowedScopes.isEmpty()) {
             log.debug("{} No allowed scope for client {}, nothing to do", getLogPrefix(), clientId);
             return;
@@ -162,8 +162,8 @@ public class ValidateScope extends AbstractOIDCAuthenticationResponseAction {
         }
 
         // These come from a request object or parameter. Absent by definition on the UserInfo endpoint.
-        Scope requestedScopes = requestedScopesLookupStrategy != null ?
-                requestedScopesLookupStrategy.apply(profileRequestContext) : null;
+        Scope requestedScopes = requestedScopeLookupStrategy != null ?
+                requestedScopeLookupStrategy.apply(profileRequestContext) : null;
         if (requestedScopes == null) {
             // With none requested, simply swap requested for previously granted, if any.
             // Set previous set to null since there's no need to filter against it.
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/impl/OIDCClientInformationEncryptionParametersResolver.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/impl/OIDCClientInformationEncryptionParametersResolver.java
index 47d30b3b..daa183f4 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/impl/OIDCClientInformationEncryptionParametersResolver.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/impl/OIDCClientInformationEncryptionParametersResolver.java
@@ -182,29 +182,28 @@ public class OIDCClientInformationEncryptionParametersResolver extends BasicEncr
             @Nonnull final CriteriaSet criteria, @Nonnull final Predicate<String> whitelistBlacklistPredicate) {
 
         if (!criteria.contains(ClientInformationCriterion.class)) {
-            log.debug("No client criterion, nothing to do");
-            super.resolveAndPopulateCredentialsAndAlgorithms(params, criteria, whitelistBlacklistPredicate);
-            return;
-        }
-        final OIDCClientInformation clientInformation =
-                criteria.get(ClientInformationCriterion.class).getOidcClientInformation();
-        if (clientInformation == null) {
-            log.debug("No client information, nothing to do");
+            log.debug("No client criterion, falling back to local configuration");
             super.resolveAndPopulateCredentialsAndAlgorithms(params, criteria, whitelistBlacklistPredicate);
             return;
         }
+        
         if (!criteria.contains(EncryptionConfigurationCriterion.class)) {
-            log.debug("No encryption configuration criterion, nothing to do");
+            log.debug("No encryption configuration criterion, falling back to default configuration");
             super.resolveAndPopulateCredentialsAndAlgorithms(params, criteria, whitelistBlacklistPredicate);
             return;
         }
+        
         final List<EncryptionConfiguration> encryptionConfigurations =
                 criteria.get(EncryptionConfigurationCriterion.class).getConfigurations();
         if (encryptionConfigurations == null || encryptionConfigurations.isEmpty()) {
-            log.debug("No encrypt configuration nothing to do");
+            log.debug("No encryption configuration, falling back to default configuration");
             super.resolveAndPopulateCredentialsAndAlgorithms(params, criteria, whitelistBlacklistPredicate);
             return;
         }
+        
+        final OIDCClientInformation clientInformation =
+                criteria.get(ClientInformationCriterion.class).getOidcClientInformation();
+        
         // We populate the parameters only for the algorithm the client has registered
         JWEAlgorithm keyTransportAlgorithm = null;
         EncryptionMethod encryptionMethod = null;
@@ -224,8 +223,7 @@ public class OIDCClientInformationEncryptionParametersResolver extends BasicEncr
                 encryptionMethod = clientInformation.getOIDCMetadata().getIDTokenJWEEnc();
         }
         if (keyTransportAlgorithm == null) {
-            log.debug("No algorithm information in client information, nothing to do");
-            criteria.add(new EncryptionOptionalCriterion(true));
+            log.debug("No algorithm information in client information, falling back to default configuration");
             super.resolveAndPopulateCredentialsAndAlgorithms(params, criteria, whitelistBlacklistPredicate);
             return;
         }
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 7990b687..d36c2058 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -206,8 +206,8 @@
         p:httpClientSecurityParameters="#{getObject('shibboleth.oidc.NonBrowser.HttpClientSecurityParameters')}" />
     
     <!--
-    This is a default souurce of "allowed" scope for access tokens. It's public to allow an overridden
-    source function to pull in the original metadata-registered scope easily.
+    This is a default souurce of "allowed" scope/audience for access tokens. It's public to allow an overridden
+    source function to pull in the original metadata-registered values easily.
     -->
     <bean id="shibboleth.oidc.DefaultAllowedScopeStrategy" parent="shibboleth.Functions.Compose">
         <constructor-arg name="g">
@@ -217,5 +217,14 @@
             <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction" />
         </constructor-arg>
     </bean>
+
+    <bean id="shibboleth.oidc.DefaultAllowedAudienceStrategy" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ClientInfoAudienceLookupFunction" />
+        </constructor-arg>
+        <constructor-arg name="f">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction" />
+        </constructor-arg>
+    </bean>
     
 </beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
index 0097db7c..0219c006 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
@@ -33,7 +33,6 @@
     <bean id="ResolveAttributes" class="net.shibboleth.idp.profile.impl.ResolveAttributes" scope="prototype"
         c:resolverService-ref="shibboleth.AttributeResolverService"
         p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
-        p:resolutionContextDecorator="#{getObject('OAuthResolutionContextDecorator')}"
         p:maskFailures="%{idp.service.attribute.resolver.maskFailures:true}" />
 
     <bean id="FilterAttributes" class="net.shibboleth.idp.profile.impl.FilterAttributes" scope="prototype"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
index c9f7de9d..6fea194a 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
@@ -172,7 +172,7 @@
         scope="prototype" />
 
     <bean id="ValidateScope" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateScope" scope="prototype"
-        p:allowedScopesLookupStrategy="#{getObject('shibboleth.oidc.AllowedScopeStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedScopeStrategy')}" />
+        p:allowedScopeLookupStrategy="#{getObject('shibboleth.oidc.AllowedScopeStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedScopeStrategy')}" />
 
     <bean id="PopulateIDTokenSignatureSigningParameters"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters " scope="prototype"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
index 73f440d0..b898c7b0 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
@@ -7,35 +7,60 @@
                            http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
     default-init-method="initialize" default-destroy-method="destroy">
 
+
+    <bean id="OIDCMetadataLookup" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+            scope="prototype" c:executionDirection="INBOUND">
+        <constructor-arg name="messageHandler">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.impl.OIDCMetadataLookupHandler" scope="prototype">
+                <property name="clientInformationResolver">
+                    <ref bean="shibboleth.ClientInformationResolver" />
+                </property>
+                <property name="clientIDLookupStrategy">
+                    <ref bean="shibboleth.ClientIDLookupStrategy" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+
+    <bean id="InitializeRelyingPartyContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeRelyingPartyContext" scope="prototype"
+        p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy" />
+        
     <util:constant id="shibboleth.MetadataLookup.Role"
         static-field="org.opensaml.saml.saml2.metadata.SPSSODescriptor.DEFAULT_ELEMENT_NAME" />    
 
     <bean id="SAMLProtocolAndRole"
             class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
-            c:executionDirection="INBOUND" p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+            c:executionDirection="INBOUND"
+            p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
         <constructor-arg name="messageHandler">
             <bean class="org.opensaml.saml.common.binding.impl.SAMLProtocolAndRoleHandler" scope="prototype"
                 p:protocol="http://openid.net/specs/openid-connect-core-1_0.html"
-                p:role-ref="shibboleth.MetadataLookup.Role" p:entityContextClass="org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext"/>
+                p:role-ref="shibboleth.MetadataLookup.Role"
+                p:entityContextClass="org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext"/>
         </constructor-arg>
     </bean>
-    
-    <bean id="SetEntityIdToSAMLPeerEntityContext"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetEntityIdToSAMLPeerEntityContext"
-        p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
-        p:entityContextClass="org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext"
-        p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}" />
 
+    <bean id="SetEntityIdToSAMLPeerEntityContext"
+            class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:executionDirection="INBOUND"
+            p:errorEvent="#{T(org.opensaml.profile.action.EventIds).INVALID_MSG_CTX}"
+            p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+        <constructor-arg name="messageHandler">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.impl.SetEntityIdToSAMLPeerEntityContext"
+                scope="prototype"
+                p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
+                p:entityContextClass="org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext" />
+        </constructor-arg>
+    </bean>
+        
     <bean id="InitializeRelyingPartyContextFromSAMLPeer"
         class="net.shibboleth.idp.saml.profile.impl.InitializeRelyingPartyContextFromSAMLPeer" scope="prototype" />
 
-    <bean id="InitializeRelyingPartyContext"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeRelyingPartyContext" scope="prototype"
-        p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy" />
-        
     <bean id="SAMLMetadataLookup"
-        class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
-        c:executionDirection="INBOUND" p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+            class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:executionDirection="INBOUND"
+            p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
         <constructor-arg name="messageHandler">
             <bean class="org.opensaml.saml.common.binding.impl.SAMLMetadataLookupHandler" scope="prototype"
                 p:entityContextClass="org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext">
@@ -47,29 +72,13 @@
         </constructor-arg>
     </bean>
 
-    <bean id="LookupSAMLPeerEntityContext" parent="shibboleth.Functions.Compose"
-        c:g-ref="shibboleth.ChildLookup.SAMLPeerEntityContext"
-        c:f-ref="shibboleth.MessageContextLookup.Inbound"/>
-
-    <bean id="LookupSAMLMetadataContext" parent="shibboleth.Functions.Compose"
-        c:g-ref="shibboleth.ChildLookup.SAMLMetadataContext"
-        c:f-ref="LookupSAMLPeerEntityContext"/>
-
     <bean id="PopulateOIDCMetadataContext"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCMetadataContext" scope="prototype"
-        p:samlMetadataContextLookupStrategy-ref="LookupSAMLMetadataContext" />
-
-    <bean id="OIDCMetadataLookup" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
-        scope="prototype" c:executionDirection="INBOUND">
+            class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:executionDirection="INBOUND"
+            p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
         <constructor-arg name="messageHandler">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.impl.OIDCMetadataLookupHandler" scope="prototype">
-                <property name="clientInformationResolver">
-                    <ref bean="shibboleth.ClientInformationResolver" />
-                </property>
-                <property name="clientIDLookupStrategy">
-                    <ref bean="shibboleth.ClientIDLookupStrategy" />
-                </property>
-            </bean>
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.impl.PopulateOIDCMetadataContext"
+                scope="prototype" />
         </constructor-arg>
     </bean>
 
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
index a5be7e28..d434469a 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
@@ -25,10 +25,12 @@
     <bean id="shibboleth.ClientIDLookupStrategy"
         class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestClientIDLookupFunction" />
 
-    <bean id="ResolveAttributesPredicate" class="net.shibboleth.oidc.profile.config.logic.ResolveAttributesPredicate" />
-    
-    <bean id="OAuthResolutionContextDecorator"
-        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.OAuthAttributeResolutionContextDecorator" />
+    <bean id="ResolveAttributesForClientPredicate"
+        class="net.shibboleth.oidc.profile.config.logic.ResolveAttributesPredicate" />
+
+    <bean id="ResolveAttributesForAudiencePredicate"
+        class="net.shibboleth.oidc.profile.config.logic.ResolveAttributesPredicate"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />
 
     <bean id="InitializeOutboundMessageContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeOutboundTokenResponseMessageContext"
@@ -156,22 +158,164 @@
     <!-- Common grant handling.  -->
 
     <bean id="ValidateScope" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateScope" scope="prototype"
-        p:requestedScopesLookupStrategy-ref="shibboleth.TokenRequestScopeLookupStrategy" />
+        p:requestedScopeLookupStrategy-ref="TokenRequestScopeLookupStrategy"
+        p:allowedScopeLookupStrategy="#{getObject('shibboleth.oidc.AllowedScopeStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedScopeStrategy')}" />
 
-    <bean id="shibboleth.TokenRequestScopeLookupStrategy"
+    <bean id="TokenRequestScopeLookupStrategy"
         class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestScopeLookupFunction" />
 
+    <bean id="ValidateAudience"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateAudience" scope="prototype"
+        p:requestedAudienceLookupStrategy-ref="TokenRequestAudienceLookupStrategy"
+        p:allowedAudienceLookupStrategy="#{getObject('shibboleth.oidc.AllowedAudienceStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedAudienceStrategy')}" />
+
+    <bean id="TokenRequestAudienceLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestAudienceLookupFunction" />
+
     <!--
-    TODO: May need to eventually conditionalize this and the encryption action following.
-    With opaque access tokens, this wouldn't be used for the client_credentials grant, but
-    with JWT access tokens it will be used for ID and access tokens. For now leaving enabled.
-    Encryption has to be redone anyway since the recipient isn't the client but the resource server.
+    Do a metadata lookup for the primary audience of the token for encryption purposes.
+    Contexts are stored under the outbound MessageContext, including the new RelyingPartyContext.
     -->
+    
+    <bean id="AudienceOIDCMetadataLookup" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+            scope="prototype" c:executionDirection="OUTBOUND">
+        <constructor-arg name="messageHandler">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.impl.OIDCMetadataLookupHandler" scope="prototype">
+                <property name="clientInformationResolver">
+                    <ref bean="shibboleth.ClientInformationResolver" />
+                </property>
+                <property name="clientIDLookupStrategy">
+                    <ref bean="AudienceClientIDLookupStrategy" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+    
+    <bean id="AudienceClientIDLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.AudienceClientIDLookupFunction" />
+
+    <bean id="InitializeAudienceRelyingPartyContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeRelyingPartyContext" scope="prototype"
+        p:relyingPartyContextCreationStrategy-ref="AudienceRelyingPartyCreationStrategy"
+        p:oidcMetadataContextLookupStrategy-ref="LookupOutboundOIDCMetadataContext"
+        p:clientIDLookupStrategy-ref="AudienceClientIDLookupStrategy"
+        p:inbound="false" />
+
+    <bean id="LookupOutboundOIDCMetadataContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction"
+        p:inbound="false" />
+
+    <bean id="AudienceRelyingPartyCreationStrategy" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookupOrCreate.RelyingPartyContext"
+        c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+
+    <bean id="AudienceSAMLProtocolAndRole"
+            class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:executionDirection="OUTBOUND"
+            p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+        <constructor-arg name="messageHandler">
+            <bean class="org.opensaml.saml.common.binding.impl.SAMLProtocolAndRoleHandler" scope="prototype"
+                p:protocol="http://openid.net/specs/openid-connect-core-1_0.html"
+                p:role-ref="shibboleth.MetadataLookup.Role"
+                p:entityContextClass="org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext"/>
+        </constructor-arg>
+    </bean>
 
-    <bean id="PopulateTokenSignatureSigningParameters"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters " scope="prototype"
-        p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
-        p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver">
+    <bean id="SetAudienceEntityIdToSAMLPeerEntityContext"
+            class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:executionDirection="OUTBOUND"
+            p:errorEvent="#{T(org.opensaml.profile.action.EventIds).INVALID_MSG_CTX}"
+            p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+        <constructor-arg name="messageHandler">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.impl.SetEntityIdToSAMLPeerEntityContext"
+                scope="prototype"
+                p:clientIDLookupStrategy-ref="AudienceClientIDLookupStrategy"
+                p:entityContextClass="org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext" />
+        </constructor-arg>
+    </bean>
+ 
+     <bean id="AudienceSAMLMetadataLookup"
+        class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+        c:executionDirection="OUTBOUND"
+        p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+        <constructor-arg name="messageHandler">
+            <bean class="org.opensaml.saml.common.binding.impl.SAMLMetadataLookupHandler" scope="prototype"
+                p:entityContextClass="org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext">
+                <property name="roleDescriptorResolver">
+                    <bean class="org.opensaml.saml.metadata.resolver.impl.PredicateRoleDescriptorResolver"
+                        c:mdResolver-ref="shibboleth.MetadataResolver" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+ 
+     <bean id="PopulateAudienceOIDCMetadataContext"
+            class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:executionDirection="OUTBOUND"
+            p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+        <constructor-arg name="messageHandler">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.impl.PopulateOIDCMetadataContext"
+                scope="prototype" />
+        </constructor-arg>
+    </bean>
+
+    <bean id="LookupPeerEntityContext" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookup.SAMLPeerEntityContext"
+        c:f-ref="shibboleth.MessageContextLookup.Outbound"/>
+
+    <bean id="LookupSAMLMetadataContext" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookup.SAMLMetadataContext"
+        c:f-ref="LookupPeerEntityContext"/>
+        
+    <bean id="InitializeAudienceRelyingPartyContextFromSAMLPeer"
+        class="net.shibboleth.idp.saml.profile.impl.InitializeRelyingPartyContextFromSAMLPeer" scope="prototype"
+        p:relyingPartyContextCreationStrategy-ref="AudienceRelyingPartyCreationStrategy"
+        p:peerEntityContextLookupStrategy-ref="LookupPeerEntityContext" />
+
+    <bean id="SelectAudienceRelyingPartyConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy"
+        p:relyingPartyConfigurationResolver-ref="shibboleth.RelyingPartyConfigurationResolver" />
+
+    <bean id="SelectAudienceProfileConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy"
+        p:failIfMissing="false" />
+
+    <bean id="EnforceAudienceConfiguration"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.EnforceAudienceConfiguration"
+        scope="prototype" />
+
+    <bean id="ResolveAttributesForAudience" class="net.shibboleth.idp.profile.impl.ResolveAttributes" scope="prototype"
+        c:resolverService-ref="shibboleth.AttributeResolverService"
+        p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
+        p:maskFailures="%{idp.service.attribute.resolver.maskFailures:true}"
+        p:issuerLookupStrategy-ref="AudienceIssuerLookupFunction"
+        p:recipientLookupStrategy-ref="AudienceIDLookupFunction" />
+
+    <bean id="FilterAttributesForAudience" class="net.shibboleth.idp.profile.impl.FilterAttributes" scope="prototype"
+        c:filterService-ref="shibboleth.AttributeFilterService"
+        p:maskFailures="%{idp.service.attribute.filter.maskFailures:true}"
+        p:issuerLookupStrategy-ref="AudienceIssuerLookupFunction"
+        p:recipientLookupStrategy-ref="AudienceIDLookupFunction"
+        p:metadataContextLookupStrategy-ref="LookupSAMLMetadataContext" />
+
+    <bean id="AudienceIssuerLookupFunction"
+        class="net.shibboleth.idp.profile.context.navigate.ResponderIdLookupFunction"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />
+
+    <bean id="AudienceIDLookupFunction"
+        class="net.shibboleth.idp.profile.context.navigate.RelyingPartyIdLookupFunction"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />
+
+    <!-- Back to token prep. -->
+    
+    <!-- Traditional third-party grant response handling. -->
+
+    <bean id="PopulateIDTokenSignatureSigningParameters"
+            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters" scope="prototype"
+            p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
+            p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver">
         <property name="securityParametersContextLookupStrategy">
             <bean parent="shibboleth.Functions.Compose"
                 c:g-ref="shibboleth.ChildLookupOrCreate.SecurityParameters"
@@ -184,7 +328,7 @@
         </property>
     </bean>
 
-    <bean id="PopulateTokenEncryptionParameters"
+    <bean id="PopulateIDTokenEncryptionParameters"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCEncryptionParameters" scope="prototype"
         p:configurationLookupStrategy-ref="shibboleth.EncryptionConfigurationLookup"
         p:encryptionParametersResolver-ref="shibboleth.oidc.EncryptionParametersResolver" />
@@ -194,8 +338,6 @@
         p:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache"
         p:keyFetchInterval="%{idp.oidc.jwksuri.fetchInterval:PT30M}" />
 
-    <!-- Traditional third-party grant response handling. -->
-
     <bean id="SetOIDCAccessTokenToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAccessTokenToResponseContext" scope="prototype"
         p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}" />
@@ -263,32 +405,72 @@
 
     <!-- client_credentials grant response actions. -->
 
+    <bean id="PopulateAccessTokenSignatureSigningParameters"
+            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters" scope="prototype"
+            p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
+            p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver"
+            p:metadataContextLookupStrategy-ref="LookupSAMLMetadataContext"
+            p:securityParametersContextLookupStrategy-ref="AudienceSecurityParametersCreationStrategy">
+        <property name="existingParametersContextLookupStrategy">
+            <bean parent="shibboleth.Functions.Compose"
+                c:g-ref="shibboleth.ChildLookup.SecurityParameters"
+                c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+        </property>
+    </bean>
+        
+    <bean id="AudienceSecurityParametersCreationStrategy" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookupOrCreate.SecurityParameters"
+        c:f-ref="AudienceRelyingPartyCreationStrategy" />
+        
+    <bean id="PopulateAccessTokenEncryptionParameters"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCEncryptionParameters" scope="prototype"
+        p:encryptionOptionalPredicate-ref="AudienceEncryptionOptionalPredicate"
+        p:oidcMetadataContextLookupStrategy-ref="LookupOutboundOIDCMetadataContext"
+        p:configurationLookupStrategy-ref="shibboleth.EncryptionConfigurationLookup"
+        p:encryptionParametersResolver-ref="shibboleth.oidc.EncryptionParametersResolver"
+        p:encryptionContextLookupStrategy-ref="AudienceEncryptionContextCreationStrategy" />
+            
+    <bean id="AudienceEncryptionOptionalPredicate"
+        class="net.shibboleth.oidc.profile.config.logic.EncryptionOptionalPredicate"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />    
+
+    <bean id="AudienceEncryptionContextCreationStrategy" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookupOrCreate.EncryptionParameters"
+        c:f-ref="AudienceRelyingPartyCreationStrategy" />
+
     <bean id="AddAttributeClaimsToAccessToken"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAttributesToClaimsSet" scope="prototype"
         p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
-        p:responseClaimsSetLookupStrategy-ref="shibboleth.AccessTokenClaimsSetLookupStrategy"
+        p:responseClaimsSetLookupStrategy-ref="AccessTokenClaimsSetLookupFunction"
         p:reservedClaimNames="#{getObject('shibboleth.oidc.AccessTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultAccessTokenReservedClaimNames')}" />
 
-    <bean id="shibboleth.AccessTokenClaimsSetLookupStrategy"
+    <bean id="AccessTokenClaimsSetLookupFunction"
         class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.AccessTokenClaimsSetLookupFunction"
         p:autoCreate="true" />
 
     <bean id="BuildAccessToken"
         class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.BuildAccessToken" scope="prototype"
         p:dataSealer="#{getObject('%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim())}"
-        p:useUnfilteredAttributes="%{idp.oauth.accessToken.useUnfilteredAttributes:true}"
-        p:scopeAttribute="#{'%{idp.oauth.accessToken.scopeAttribute:scope}'.trim()}"
-        p:audienceAttribute="#{'%{idp.oauth.accessToken.audienceAttribute:audience}'.trim()}" />
+        p:issuerLookupStrategy-ref="AudienceIssuerLookupFunction"
+        p:accessTokenTypeLookupStrategy-ref="AccessTokenTypeLookupFunction"
+        p:accessTokenLifetimeLookupStrategy-ref="AccessTokenLifetimeLookupFunction" />
+
+    <bean id="AccessTokenTypeLookupFunction"
+        class="net.shibboleth.oidc.profile.config.navigate.AccessTokenTypeLookupFunction"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />
         
+    <bean id="AccessTokenLifetimeLookupFunction"
+        class="net.shibboleth.oidc.profile.config.navigate.AccessTokenLifetimeLookupFunction"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />
+
     <bean id="SignAccessToken"
-            class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SignAccessToken" scope="prototype"
-            p:typeHeader="at+jwt">
-        <property name="securityParametersLookupStrategy">
-            <bean parent="shibboleth.Functions.Compose"
-                c:g-ref="shibboleth.ChildLookup.SecurityParameters"
-                c:f-ref="shibboleth.ChildLookup.RelyingParty" />
-        </property>
-    </bean>
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SignAccessToken" scope="prototype"
+        p:securityParametersLookupStrategy-ref="AudienceSecurityParametersCreationStrategy"
+        p:typeHeader="at+jwt" />
+
+    <bean id="EncryptAccessToken"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.EncryptAccessToken" scope="prototype"
+        p:encryptionContextLookupStrategy-ref="AudienceEncryptionContextCreationStrategy" />
     
     <bean id="SetOAuthAccessTokenToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SetAccessTokenToResponseContext"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-flow.xml
index de879956..b8222360 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-flow.xml
@@ -8,6 +8,7 @@
         <evaluate expression="FlowStartPopulateAuditContext" />
         <evaluate expression="InitializeOutboundMessageContext" />
         <evaluate expression="'proceed'" />
+        
         <transition on="proceed" to="DecodeMessage" />
     </action-state>
 
@@ -15,16 +16,18 @@
         <evaluate expression="DecodeMessage" />
         <evaluate expression="PostDecodePopulateAuditContext" />
         <evaluate expression="'proceed'" />
+        
         <!-- DoMetadataLookup is expected to proceed to SelectConfiguration -->
         <transition on="proceed" to="DoMetadataLookup" />
     </action-state>
 
     <action-state id="SelectConfiguration">
-        <evaluate expression="SelectRelyingPartyConfiguration" />        
+        <evaluate expression="SelectRelyingPartyConfiguration" />
         <evaluate expression="SelectProfileConfiguration" />
         <evaluate expression="PostLookupPopulateAuditContext" />
         <evaluate expression="PopulateTokenEndpointJwtSignatureValidationParameters"/>
         <evaluate expression="'proceed'" />
+        
         <transition on="proceed" to="AuthenticationSetup" />
     </action-state>
 
@@ -33,6 +36,7 @@
     <action-state id="ResumeAfterAuthentication">
         <evaluate expression="ValidateGrantType" />
         <evaluate expression="'proceed'" />
+        
         <transition on="proceed" to="BranchOnGrantType" />
     </action-state>
 
@@ -54,8 +58,10 @@
         <evaluate expression="SetConsentToResponseContext" />
         <evaluate expression="InitializeSubjectContext" />
         <evaluate expression="SetSubjectFromAuthzCodeToResponseContext" />
+        <evaluate expression="ValidateScope" />
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="CommonGrantProcessing" />
+        
+        <transition on="proceed" to="CheckAttributeResolutionForClient" />
     </action-state>
 
     <!-- These steps apply to grants that are self-contained on this endpoint. -->
@@ -63,34 +69,80 @@
         <evaluate expression="SetAuthenticationContextClassReferenceToResponseContext" />
         <evaluate expression="SetAuthenticationTimeToResponseContext" />
         <evaluate expression="SetSubjectFromSubjectContextToResponseContext" />
+        <evaluate expression="ValidateScope" />
+        <evaluate expression="ValidateAudience" />
+        
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="CommonGrantProcessing" />
+        <transition on="proceed" to="LookupAudienceMetadata" />
     </action-state>
 
-    <!-- These steps are common to all grant types. -->
-    <action-state id="CommonGrantProcessing">
-        <evaluate expression="ValidateScope" />
-        <evaluate expression="PopulateTokenSignatureSigningParameters" />
-        <evaluate expression="PopulateTokenEncryptionParameters" />
+    <!-- For client credentials grant, need to flip the Relying Party here to the primary resource/audience. -->
+
+    <action-state id="LookupAudienceMetadata">
+        <evaluate expression="AudienceOIDCMetadataLookup" />
+        <evaluate expression="InitializeAudienceRelyingPartyContext" />
+        <evaluate expression="'proceed'" />
+    
+        <transition on="proceed" to="CheckIfAudienceFoundFromClientInformationService" />
+    </action-state>
+    
+    <decision-state id="CheckIfAudienceFoundFromClientInformationService">
+        <if test="opensamlProfileRequestContext.getOutboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext))"
+            then="SelectAudienceProfileConfiguration" else="LookupAudienceSAMLMetadata" />
+    </decision-state>
+    
+    <action-state id="LookupAudienceSAMLMetadata">
+        <evaluate expression="AudienceSAMLProtocolAndRole" />
+        <evaluate expression="SetAudienceEntityIdToSAMLPeerEntityContext" />
+        <evaluate expression="AudienceSAMLMetadataLookup" />
+        <evaluate expression="'proceed'" />
+
+        <transition on="proceed" to="CheckIfAudienceFoundFromSAMLMetadata" />
+    </action-state>
+
+    <decision-state id="CheckIfAudienceFoundFromSAMLMetadata">
+        <if test="opensamlProfileRequestContext.getOutboundMessageContext().containsSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext)) and opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext)).containsSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLMetadataContext))"
+            then="PopulateAudienceOIDCMetadataContextFromSAML"
+            else="SelectAudienceProfileConfiguration" />
+    </decision-state>
+    
+    <action-state id="PopulateAudienceOIDCMetadataContextFromSAML">
+        <evaluate expression="PopulateAudienceOIDCMetadataContext" />
+        <evaluate expression="InitializeAudienceRelyingPartyContextFromSAMLPeer" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="SelectAudienceProfileConfiguration" />
+    </action-state>
+
+    <action-state id="SelectAudienceProfileConfiguration">
+        <evaluate expression="SelectAudienceRelyingPartyConfiguration" />
+        <evaluate expression="SelectAudienceProfileConfiguration" />
+        <evaluate expression="'proceed'" />
+
+        <transition on="proceed" to="EnforceAudienceConfiguration" />
+    </action-state>
+
+    <action-state id="EnforceAudienceConfiguration">
+        <evaluate expression="EnforceAudienceConfiguration" />
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="CheckAttributeResolution" />
+        
+        <transition on="proceed" to="CheckAttributeResolutionForAudience" />
     </action-state>
 
-    <decision-state id="CheckAttributeResolution">
-        <if test="ResolveAttributesPredicate.test(opensamlProfileRequestContext)"
-            then="AttributeResolution"
+    <decision-state id="CheckAttributeResolutionForClient">
+        <if test="ResolveAttributesForClientPredicate.test(opensamlProfileRequestContext)"
+            then="AttributeResolutionForClient"
             else="DoConsentLookup" />
     </decision-state>
 
-    <action-state id="AttributeResolution">
+    <action-state id="AttributeResolutionForClient">
         <evaluate expression="ResolveAttributes" />
         <evaluate expression="FilterAttributes" />
         <evaluate expression="'proceed'" />
+        
         <transition on="proceed" to="DoConsentLookup" />
     </action-state>
     
-    <!-- Consent check is not applicable to some grant types but does no harm. -->
-    
     <decision-state id="BuildResponse">
         <if test="NotClientCredentialsGrantCondition.test(opensamlProfileRequestContext)"
             then="TraditionalGrantResponse"
@@ -98,6 +150,8 @@
     </decision-state>
     
     <action-state id="TraditionalGrantResponse">
+        <evaluate expression="PopulateIDTokenSignatureSigningParameters" />
+        <evaluate expression="PopulateIDTokenEncryptionParameters" />
         <evaluate expression="SetOIDCAccessTokenToResponseContext" />
         <evaluate expression="SetRefreshTokenToResponseContext" />
         <evaluate expression="AddIDTokenShell" />
@@ -110,15 +164,34 @@
         <evaluate expression="SignIDToken" />
         <evaluate expression="EncryptIDToken" />
         <evaluate expression="'proceed'" />
+        
         <transition on="proceed" to="BuildResponseMessage" />
     </action-state>
 
+    <decision-state id="CheckAttributeResolutionForAudience">
+        <if test="ResolveAttributesForAudiencePredicate.test(opensamlProfileRequestContext)"
+            then="AttributeResolutionForAudience"
+            else="ClientCredentialsGrantResponse" />
+    </decision-state>
+
+    <action-state id="AttributeResolutionForAudience">
+        <evaluate expression="ResolveAttributesForAudience" />
+        <evaluate expression="FilterAttributesForAudience" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="ClientCredentialsGrantResponse" />
+    </action-state>
+
     <action-state id="ClientCredentialsGrantResponse">
+        <evaluate expression="PopulateAccessTokenSignatureSigningParameters" />
+        <evaluate expression="PopulateAccessTokenEncryptionParameters" />
         <evaluate expression="AddAttributeClaimsToAccessToken" />
         <evaluate expression="BuildAccessToken" />
         <evaluate expression="SignAccessToken" />
+        <evaluate expression="EncryptAccessToken" />
         <evaluate expression="SetOAuthAccessTokenToResponseContext" />
         <evaluate expression="'proceed'" />
+        
         <transition on="proceed" to="BuildResponseMessage" />
     </action-state>
 
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
index fd8dd794..e902612a 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
@@ -35,8 +35,8 @@
         class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.UserInfoRequestClientIDLookupFunction" />
 
     <bean id="ValidateScope" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateScope" scope="prototype"
-            p:allowedScopesLookupStrategy="#{getObject('shibboleth.oidc.AllowedScopeStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedScopeStrategy')}">
-        <property name="requestedScopesLookupStrategy">
+            p:allowedScopeLookupStrategy="#{getObject('shibboleth.oidc.AllowedScopeStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedScopeStrategy')}">
+        <property name="requestedScopeLookupStrategy">
             <null/>
         </property>
     </bean>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 46762697..960407b3 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -22,8 +22,10 @@
         p:issuer-ref="issuer"
         p:tokenEndpointAuthMethods="%{idp.oidc.tokenEndpointAuthMethods:client_secret_basic,client_secret_post,client_secret_jwt,private_key_jwt}"
         p:claimsValidator-ref="DefaultJWTClaimsValidator"
+        p:encryptionOptional="%{idp.oidc.encryptionOptional:true}"
         p:forcePKCE="%{idp.oidc.forcePKCE:false}"
-        p:allowPKCEPlain="%{idp.oidc.allowPKCEPlain:false}" 
+        p:allowPKCEPlain="%{idp.oidc.allowPKCEPlain:false}"
+        p:allowUntrustedAudience="%{idp.oauth2.allowUntrustedAudience:false}"
         p:iDTokenLifetime="%{idp.oidc.idToken.defaultLifetime:PT1H}"
         p:accessTokenLifetime="%{idp.oidc.accessToken.defaultLifetime:PT10M}"
         p:refreshTokenLifetime="%{idp.oidc.refreshToken.defaultLifetime:PT2H}"
@@ -43,6 +45,7 @@
     <bean id="OIDC.UserInfo" parent="AbstractOIDCProfile" lazy-init="true"
         class="net.shibboleth.oidc.profile.config.OIDCUserInfoConfiguration"
         p:issuer-ref="issuer"
+        p:encryptionOptional="%{idp.oidc.encryptionOptional:true}"
         p:deniedUserInfoAttributes="%{idp.oidc.deniedUserInfoAttributes:}" />
         
     <bean id="OIDC.Registration" parent="AbstractOIDCProfile" lazy-init="true"
@@ -176,6 +179,22 @@
                 <constructor-arg value="true" />
             </bean>
         </property>
+        <property name="encryptionOptionalPredicate">
+            <bean class="net.shibboleth.utilities.java.support.logic.PredicateSupport" factory-method="fromFunction">
+                <constructor-arg>
+                    <bean parent="shibboleth.MDDrivenBoolProperty" p:propertyName="encryptionOptional" />
+                </constructor-arg>
+                <constructor-arg value="%{idp.oidc.encryptionOptional:true}" />
+            </bean>
+        </property>
+        <property name="allowUntrustedAudiencePredicate">
+            <bean class="net.shibboleth.utilities.java.support.logic.PredicateSupport" factory-method="fromFunction">
+                <constructor-arg>
+                    <bean parent="shibboleth.MDDrivenBoolProperty" p:propertyName="allowUntrustedAudience" />
+                </constructor-arg>
+                <constructor-arg value="%{idp.oauth2.allowUntrustedAudience:false}" />
+            </bean>
+        </property>
         <property name="iDTokenLifetimeLookupStrategy">
             <bean parent="shibboleth.MDDrivenDurationProperty" p:propertyName="iDTokenLifetime"
                 p:defaultValue="%{idp.oidc.idToken.defaultLifetime:PT1H}" />
@@ -238,7 +257,6 @@
                 </property>
             </bean>
         </property>
-        <!-- Last 3 are marked for removal. -->
         <property name="forcePKCEPredicate">
             <bean class="net.shibboleth.utilities.java.support.logic.PredicateSupport" factory-method="fromFunction">
                 <constructor-arg>
@@ -290,6 +308,14 @@
         <property name="issuerLookupStrategy">
             <bean parent="shibboleth.MDDrivenStringProperty" p:propertyName="issuer" p:defaultValue-ref="issuer" />
         </property>
+        <property name="encryptionOptionalPredicate">
+            <bean class="net.shibboleth.utilities.java.support.logic.PredicateSupport" factory-method="fromFunction">
+                <constructor-arg>
+                    <bean parent="shibboleth.MDDrivenBoolProperty" p:propertyName="encryptionOptional" />
+                </constructor-arg>
+                <constructor-arg value="%{idp.oidc.encryptionOptional:true}" />
+            </bean>
+        </property>
         <property name="deniedUserInfoAttributesLookupStrategy">
             <bean parent="shibboleth.MDDrivenSetProperty" p:propertyName="deniedUserInfoAttributes">
                 <property name="defaultValue">
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetEntityIdToSAMLPeerEntityContextTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/impl/SetEntityIdToSAMLPeerEntityContextTest.java
similarity index 61%
rename from idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetEntityIdToSAMLPeerEntityContextTest.java
rename to idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/impl/SetEntityIdToSAMLPeerEntityContextTest.java
index ea6aca21..6fdd01a4 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetEntityIdToSAMLPeerEntityContextTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/impl/SetEntityIdToSAMLPeerEntityContextTest.java
@@ -15,27 +15,20 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+package net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.impl;
 
 import java.util.function.Function;
 
 import org.mockito.Mockito;
 import org.opensaml.messaging.context.MessageContext;
-import org.opensaml.profile.action.EventIds;
-import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
 import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
-import org.springframework.webflow.execution.Event;
-import org.springframework.webflow.execution.RequestContext;
 import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import com.nimbusds.oauth2.sdk.id.ClientID;
 
-import net.shibboleth.idp.profile.testing.ActionTestingSupport;
-import net.shibboleth.idp.profile.testing.RequestContextBuilder;
-import net.shibboleth.idp.plugin.oidc.op.profile.impl.SetEntityIdToSAMLPeerEntityContext;
-import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.UnmodifiableComponentException;
 import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
@@ -45,78 +38,72 @@ import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
  */
 public class SetEntityIdToSAMLPeerEntityContextTest {
 
-    private SetEntityIdToSAMLPeerEntityContext action;
+    private SetEntityIdToSAMLPeerEntityContext handler;
     
-    private RequestContext requestCtx;
-
-    private ProfileRequestContext prc;
+    private MessageContext messageCtx;
     
     private String clientId = "mockClientId";
     
     @BeforeMethod
     public void setup() {
-        action = new SetEntityIdToSAMLPeerEntityContext();
+        handler = new SetEntityIdToSAMLPeerEntityContext();
     }
     
     public void initialize(final SAMLPeerEntityContext entityContext, 
             final Function<MessageContext, ClientID> clientIdLookupStrategy) throws ComponentInitializationException {
-        action.setClientIDLookupStrategy(clientIdLookupStrategy);
-        action.initialize();
-        requestCtx = new RequestContextBuilder().buildRequestContext();
-        prc = new WebflowRequestContextProfileRequestContextLookup().apply(requestCtx);
+        handler.setClientIDLookupStrategy(clientIdLookupStrategy);
+        handler.initialize();
+        messageCtx = new MessageContext();
         if (entityContext != null) {
-            prc.getInboundMessageContext().addSubcontext(entityContext);
+            messageCtx.addSubcontext(entityContext);
         }
     }
     
     @Test(expectedExceptions = { ConstraintViolationException.class })
     public void setClientIDLookupStrategy_shouldThrowIfGivenNull() {
-        action.setClientIDLookupStrategy(null);
+        handler.setClientIDLookupStrategy(null);
     }
     
     @Test(expectedExceptions = { UnmodifiableComponentException.class })
     public void setClientIDLookupStrategy_shouldThrowIfCalledAfterInitialization()
             throws ComponentInitializationException {
-        action.initialize();
-        action.setClientIDLookupStrategy(null);
+        handler.initialize();
+        handler.setClientIDLookupStrategy(null);
     }
 
     @Test(expectedExceptions = { ConstraintViolationException.class })
     public void setEntityContextClass_shouldThrowIfGivenNull() {
-        action.setEntityContextClass(null);
+        handler.setEntityContextClass(null);
     }
     
     @Test(expectedExceptions = { UnmodifiableComponentException.class })
-    public void setEntityContextClass_shouldThrowIfCalledAfterInitialization()
-            throws ComponentInitializationException {
-        action.initialize();
-        action.setEntityContextClass(null);
+    public void setEntityContextClass_shouldThrowIfCalledAfterInitialization() throws ComponentInitializationException {
+        handler.initialize();
+        handler.setEntityContextClass(null);
     }
     
-    @Test
-    public void execute_shouldSetInvalidMsgEventIfNoEntityContext() throws ComponentInitializationException {
+    @Test(expectedExceptions=MessageHandlerException.class)
+    public void execute_shouldSetInvalidMsgEventIfNoEntityContext() throws ComponentInitializationException,
+            MessageHandlerException {
         initialize(null, mockClientIdLookupStrategy(clientId));
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MSG_CTX);
+        handler.invoke(messageCtx);
     }
 
     @Test
-    public void execute_shouldSetNullClientIdWhenLookupReturnsNull() throws ComponentInitializationException {
+    public void execute_shouldSetNullClientIdWhenLookupReturnsNull() throws ComponentInitializationException,
+            MessageHandlerException {
         initialize(new SAMLPeerEntityContext(), mockClientIdLookupStrategy(null));
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertProceedEvent(event);
-        final SAMLPeerEntityContext entityCtx =
-                prc.getInboundMessageContext().getSubcontext(SAMLPeerEntityContext.class);
+        handler.invoke(messageCtx);
+        final SAMLPeerEntityContext entityCtx = messageCtx.getSubcontext(SAMLPeerEntityContext.class);
         Assert.assertNull(entityCtx.getEntityId());
     }
 
     @Test
-    public void execute_shouldSetClientIdWhenPrequisitesMet() throws ComponentInitializationException {
+    public void execute_shouldSetClientIdWhenPrequisitesMet() throws ComponentInitializationException,
+            MessageHandlerException {
         initialize(new SAMLPeerEntityContext(), mockClientIdLookupStrategy(clientId));
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertProceedEvent(event);
-        final SAMLPeerEntityContext entityCtx =
-                prc.getInboundMessageContext().getSubcontext(SAMLPeerEntityContext.class);
+        handler.invoke(messageCtx);
+        final SAMLPeerEntityContext entityCtx = messageCtx.getSubcontext(SAMLPeerEntityContext.class);
         Assert.assertEquals(entityCtx.getEntityId(), clientId);
     }
 
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
index b345a871..d883801b 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
@@ -21,11 +21,6 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
 
-import net.shibboleth.idp.attribute.EmptyAttributeValue;
-import net.shibboleth.idp.attribute.EmptyAttributeValue.EmptyType;
-import net.shibboleth.idp.attribute.IdPAttribute;
-import net.shibboleth.idp.attribute.StringAttributeValue;
-import net.shibboleth.idp.attribute.context.AttributeContext;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.AccessTokenContext;
 import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
@@ -37,15 +32,12 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationEx
 import net.shibboleth.utilities.java.support.logic.FunctionSupport;
 import net.shibboleth.utilities.java.support.security.DataSealerException;
 
-import java.net.URI;
-import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -56,11 +48,7 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.oauth2.sdk.ClientCredentialsGrant;
 import com.nimbusds.oauth2.sdk.Scope;
-import com.nimbusds.oauth2.sdk.TokenRequest;
-import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
-import com.nimbusds.oauth2.sdk.auth.Secret;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 
 // Checkstyle: ThrowsCount OFF
@@ -75,41 +63,15 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
     protected void setUp() throws Exception {
         super.setUp();
         
-        final TokenRequest req = new TokenRequest(new URI("http://localhost"),
-                new ClientSecretBasic(new ClientID(clientId), new Secret("foo")),
-                new ClientCredentialsGrant(),
-                Scope.parse("email profile"),
-                Collections.singletonList(new URI("https://rp.example.org")),
-                null);
-        profileRequestCtx.getInboundMessageContext().setMessage(req);
-        
-        respCtx.setScope(new Scope());
         respCtx.setAuthTime(Instant.now());
         respCtx.setSubject(clientId);
         respCtx.setAcr("0");
+        respCtx.setScope(new Scope());
+        respCtx.getAudience().add("https://rp.example.org");
         
         action = new BuildAccessToken();
     }
 
-    /**
-     * Test invalid message cases.
-     * 
-     * @throws ComponentInitializationException 
-     * @throws NoSuchAlgorithmException 
-     */
-    @Test
-    public void testBadRequest() throws NoSuchAlgorithmException, ComponentInitializationException {
-        initAction(null, null, null);
-        
-        profileRequestCtx.getInboundMessageContext().setMessage(request);
-        Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MSG_CTX);
-
-        profileRequestCtx.getInboundMessageContext().setMessage(null);
-        event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MSG_CTX);
-    }
-
     /**
      * Test no client ID.
      * 
@@ -119,89 +81,15 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
     @Test
     public void testNoClientID() throws NoSuchAlgorithmException, ComponentInitializationException {
         action.setClientIDLookupStrategy(FunctionSupport.constant(null));
-        action.setScopeAttribute(null);
-        action.setAudienceAttribute(null);
         action.setDataSealer(getDataSealer());
         action.initialize();
         
         final Event event = action.execute(requestCtx);
         ActionTestingSupport.assertEvent(event, EventIds.MESSAGE_PROC_ERROR);
     }
-
-    /**
-     * Test no source of audience claim.
-     * 
-     * @throws ParseException 
-     * @throws DataSealerException 
-     * @throws ComponentInitializationException 
-     * @throws NoSuchAlgorithmException
-     * @throws URISyntaxException 
-     */
-    @Test
-    public void testNoAudience() throws ParseException, DataSealerException, ComponentInitializationException,
-            NoSuchAlgorithmException, URISyntaxException {
-
-        final TokenRequest req = new TokenRequest(new URI("http://localhost"),
-                new ClientSecretBasic(new ClientID(clientId), new Secret("foo")),
-                new ClientCredentialsGrant(),
-                Scope.parse("email profile"));
-        profileRequestCtx.getInboundMessageContext().setMessage(req);
-
-        initAction(null, null, null);
-        
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, EventIds.ACCESS_DENIED);
-    }
-
-
-    /**
-     * Test no source of audience claim from attribute.
-     * 
-     * @throws ParseException 
-     * @throws DataSealerException 
-     * @throws ComponentInitializationException 
-     * @throws NoSuchAlgorithmException
-     * @throws URISyntaxException 
-     */
-    @Test
-    public void testNoAudienceFromAttribute() throws ParseException, DataSealerException,
-            ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException {
-
-        initAction(null, null, "aud");
-
-        final AttributeContext attributeCtx = rpCtx.getSubcontext(AttributeContext.class, true);
-        attributeCtx.setUnfilteredIdPAttributes(Collections.singletonList(new IdPAttribute("aud")));
-        
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, EventIds.ACCESS_DENIED);
-    }
-
-    /**
-     * Test no source of scope claim from attribute.
-     * 
-     * @throws ParseException 
-     * @throws DataSealerException 
-     * @throws ComponentInitializationException 
-     * @throws NoSuchAlgorithmException
-     * @throws URISyntaxException 
-     */
-    @Test
-    public void testNoScopeFromAttribute() throws ParseException, DataSealerException,
-            ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException {
-
-        initAction(null, "scope", null);
-
-        final AttributeContext attributeCtx = rpCtx.getSubcontext(AttributeContext.class, true);
-        attributeCtx.setUnfilteredIdPAttributes(Collections.singletonList(new IdPAttribute("foo")));
-        
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertProceedEvent(event);
-        verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
-                Collections.singletonList("https://rp.example.org"));
-    }
     
     /**
-     * Basic success case, direct reuse of requested scope/audience.
+     * Basic success case.
      * 
      * @throws ParseException 
      * @throws DataSealerException 
@@ -212,7 +100,7 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
     public void testOpaqueSuccess() throws ParseException, DataSealerException, ComponentInitializationException,
             NoSuchAlgorithmException {
 
-        initAction(null, null, null);
+        initAction(null);
         
         final Event event = action.execute(requestCtx);
         ActionTestingSupport.assertProceedEvent(event);
@@ -220,62 +108,6 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
         verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
                 Collections.singletonList("https://rp.example.org"));
     }
-
-    /**
-     * Basic success case, resolve scope.
-     * 
-     * @throws ParseException 
-     * @throws DataSealerException 
-     * @throws ComponentInitializationException 
-     * @throws NoSuchAlgorithmException
-     */
-    @Test
-    public void testOpaqueSuccessWithScopeResolution() throws ParseException, DataSealerException,
-            ComponentInitializationException, NoSuchAlgorithmException {
-
-        initAction(null, "scope", null);
-        
-        final AttributeContext attributeCtx = rpCtx.getSubcontext(AttributeContext.class, true);
-        final IdPAttribute scope = new IdPAttribute("scope");
-        scope.setValues(List.of(new StringAttributeValue("foo"),
-                new EmptyAttributeValue(EmptyType.NULL_VALUE),
-                new StringAttributeValue("bar")));
-        attributeCtx.setUnfilteredIdPAttributes(List.of(scope));
-        
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertProceedEvent(event);
-        
-        verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), Scope.parse("foo bar"),
-                Collections.singletonList("https://rp.example.org"));
-    }
-
-    /**
-     * Basic success case, resolve audience.
-     * 
-     * @throws ParseException 
-     * @throws DataSealerException 
-     * @throws ComponentInitializationException 
-     * @throws NoSuchAlgorithmException
-     */
-    @Test
-    public void testOpaqueSuccessWithAudResolution() throws ParseException, DataSealerException,
-            ComponentInitializationException, NoSuchAlgorithmException {
-
-        initAction(null, null, "aud");
-        
-        final AttributeContext attributeCtx = rpCtx.getSubcontext(AttributeContext.class, true);
-        final IdPAttribute aud = new IdPAttribute("aud");
-        aud.setValues(List.of(new StringAttributeValue("foo"),
-                new EmptyAttributeValue(EmptyType.NULL_VALUE),
-                new StringAttributeValue("bar")));
-        attributeCtx.setUnfilteredIdPAttributes(List.of(aud));
-        
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertProceedEvent(event);
-        
-        verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
-                List.of("foo", "bar"));
-    }
     
     /**
      * Basic success case, direct reuse of requested scope/audience.
@@ -289,7 +121,7 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
     public void testJWTSuccess() throws ParseException, ComponentInitializationException, NoSuchAlgorithmException,
             DataSealerException {
 
-        initAction(null, null, null);
+        initAction("JWT");
         
         final Event event = action.execute(requestCtx);
         ActionTestingSupport.assertProceedEvent(event);
@@ -297,84 +129,23 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
         verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
                 Collections.singletonList("https://rp.example.org"));
     }
-
-    /**
-     * Basic success case, resolve scope.
-     * 
-     * @throws ParseException 
-     * @throws ComponentInitializationException 
-     * @throws NoSuchAlgorithmException
-     * @throws DataSealerException 
-     */
-    @Test
-    public void testJWTSuccessWithScopeResolution() throws ParseException, ComponentInitializationException,
-            NoSuchAlgorithmException, DataSealerException {
-
-        initAction(null, "scope", null);
-        
-        final AttributeContext attributeCtx = rpCtx.getSubcontext(AttributeContext.class, true);
-        final IdPAttribute scope = new IdPAttribute("scope");
-        scope.setValues(List.of(new StringAttributeValue("foo"),
-                new EmptyAttributeValue(EmptyType.NULL_VALUE),
-                new StringAttributeValue("bar")));
-        attributeCtx.setUnfilteredIdPAttributes(List.of(scope));
-        
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertProceedEvent(event);
-        
-        verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), Scope.parse("foo bar"),
-                Collections.singletonList("https://rp.example.org"));
-    }
-
-    /**
-     * Basic success case, resolve audience.
-     * 
-     * @throws ParseException 
-     * @throws DataSealerException 
-     * @throws ComponentInitializationException 
-     * @throws NoSuchAlgorithmException
-     */
-    @Test
-    public void testJWTSuccessWithAudResolution() throws ParseException, DataSealerException,
-            ComponentInitializationException, NoSuchAlgorithmException {
-
-        initAction("JWT", null, "aud");
-        
-        final AttributeContext attributeCtx = rpCtx.getSubcontext(AttributeContext.class, true);
-        final IdPAttribute aud = new IdPAttribute("aud");
-        aud.setValues(List.of(new StringAttributeValue("foo"),
-                new EmptyAttributeValue(EmptyType.NULL_VALUE),
-                new StringAttributeValue("bar")));
-        attributeCtx.setUnfilteredIdPAttributes(List.of(aud));
-        
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertProceedEvent(event);
-        
-        verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
-                List.of("foo", "bar"));
-    }
     
     /**
      * Init action bean based on test.
      * 
      * @param type token type to use
-     * @param scope scope attribute ID
-     * @param audience audience attribute ID
      * 
      * @throws ComponentInitializationException
      * @throws NoSuchAlgorithmException
      */
-    private void initAction(@Nullable @NotEmpty final String type, @Nullable @NotEmpty final String scope,
-            @Nullable @NotEmpty final String audience)
+    private void initAction(@Nullable @NotEmpty final String type)
                     throws ComponentInitializationException, NoSuchAlgorithmException {
         if ("JWT".equals(type)) {
             action.setAccessTokenTypeLookupStrategy(FunctionSupport.constant("JWT"));
         } else if (type == null) {
             action.setDataSealer(getDataSealer());
         }
-
-        action.setScopeAttribute(scope);
-        action.setAudienceAttribute(audience);
+        action.setClientIDLookupStrategy(FunctionSupport.constant(new ClientID(clientId)));
         action.initialize();
     }
 
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudienceTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudienceTest.java
new file mode 100644
index 00000000..22996232
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudienceTest.java
@@ -0,0 +1,219 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.plugin.oidc.op.oauth2.profile.impl;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Instant;
+import java.util.List;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+import org.opensaml.profile.action.EventIds;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.ClientCredentialsGrant;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.claims.ACR;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+/** {@link ValidateAudience} unit test. */
+public class ValidateAudienceTest extends BaseOIDCResponseActionTest {
+
+    /** Action to test. */
+    private ValidateAudience action;
+
+    /** Client metadata. */
+    private OIDCClientMetadata metaData;
+
+    @BeforeMethod
+    private void init() throws ComponentInitializationException, URISyntaxException {
+        action = new ValidateAudience();
+        action.initialize();
+        final OIDCMetadataContext oidcCtx =
+                profileRequestCtx.getInboundMessageContext().getSubcontext(OIDCMetadataContext.class, true);
+        metaData = new OIDCClientMetadata();
+        metaData.setCustomField("audience", List.of("https://sp.example.org", "https://sp2.example.org"));
+        final OIDCClientInformation information =
+                new OIDCClientInformation(new ClientID("test"), null, metaData, null, null, null);
+        oidcCtx.setClientInformation(information);
+    }
+
+    /**
+     * Test that action filters out unregistered audiences on back-channel with no prior grant.
+     * 
+     * <p>This is most common for the client_credentials grant, which has no prior step.</p>
+     * 
+     * @throws ComponentInitializationException
+     * @throws URISyntaxException
+     */
+    @Test
+    public void testTokenNoneAllowed() throws ComponentInitializationException, URISyntaxException {
+
+        final TokenRequest req = new TokenRequest(new URI("http://localhost"),
+                new ClientSecretBasic(new ClientID("s6BhdRkqt3"), new Secret("foo")),
+                new ClientCredentialsGrant(),
+                null,
+                List.of(new URI("https://sp3.example.org")),
+                null
+                );
+        setTokenRequest(req);
+        
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertEvent(event, EventIds.ACCESS_DENIED);
+    }
+    
+    /**
+     * Test that action filters out unregistered audiences on back-channel with no prior grant
+     * and includes the registered values.
+     * 
+     * @throws ComponentInitializationException
+     * @throws URISyntaxException
+     */
+    @Test
+    public void testTokenSuccess() throws ComponentInitializationException, URISyntaxException {
+
+        final TokenRequest req = new TokenRequest(new URI("http://localhost"),
+                new ClientSecretBasic(new ClientID("s6BhdRkqt3"), new Secret("foo")),
+                new ClientCredentialsGrant(),
+                null,
+                List.of(new URI("https://sp.example.org"),
+                        new URI("https://sp3.example.org"),
+                        new URI("https://sp2.example.org")),
+                null
+                );
+        setTokenRequest(req);
+        
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        Assert.assertEquals(respCtx.getAudience(),
+                List.of("https://sp.example.org", "https://sp2.example.org"));
+    }
+    
+    /**
+     * Test that action filters out unregistered audiences on back-channel with no prior grant
+     * and selects the first registered value.
+     * 
+     * @throws ComponentInitializationException
+     * @throws URISyntaxException
+     */
+    @Test
+    public void testTokenFirstAudience() throws ComponentInitializationException, URISyntaxException {
+
+        final TokenRequest req = new TokenRequest(new URI("http://localhost"),
+                new ClientSecretBasic(new ClientID("s6BhdRkqt3"), new Secret("foo")),
+                new ClientCredentialsGrant(),
+                null
+                );
+        setTokenRequest(req);
+        
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        Assert.assertEquals(respCtx.getAudience(), List.of("https://sp.example.org"));
+    }
+    
+   /**
+    * Test that action filters out unregistered audiences on back-channel with prior grant of nothing.
+    * 
+    * <p>This is likely impossible in practice, but theoretical.</p>
+    * 
+    * @throws ComponentInitializationException
+    * @throws URISyntaxException
+    */
+   @Test
+   public void testTokenFailNonePrevGranted() throws ComponentInitializationException, URISyntaxException {
+       
+       final TokenRequest req = new TokenRequest(new URI("http://localhost"),
+               new ClientSecretBasic(new ClientID("s6BhdRkqt3"), new Secret("foo")),
+               new AuthorizationCodeGrant(new AuthorizationCode("foo"), new URI("http://localhost")),
+               null,
+               List.of(new URI("https://sp.example.org")),
+               null);
+       setTokenRequest(req);
+       
+       final AuthorizeCodeClaimsSet claims = new AuthorizeCodeClaimsSet.Builder()
+                   .setJWTID(idGenerator)
+                   .setClientID(new ClientID("s6BhdRkqt3"))
+                   .setIssuer("issuer")
+                   .setPrincipal("userPrin")
+                   .setSubject("subject")
+                   .setIssuedAt(Instant.now())
+                   .setExpiresAt(Instant.now())
+                   .setAuthenticationTime(Instant.now())
+                   .setRedirectURI(new URI("http://localhost"))
+                   .setACR(new ACR("0"))
+                   .setScope(new Scope())
+                   .build();
+       respCtx.setAuthorizationGrantClaimsSet(claims);
+       
+       final Event event = action.execute(requestCtx);
+       ActionTestingSupport.assertEvent(event, EventIds.ACCESS_DENIED);
+   }
+
+   /**
+    * Test that action filters out unregistered audiences on back-channel with a prior grant.
+    * 
+    * @throws ComponentInitializationException
+    * @throws URISyntaxException
+    */
+   @Test
+   public void testTokenFailPrevGranted() throws ComponentInitializationException, URISyntaxException {
+       
+       final TokenRequest req = new TokenRequest(new URI("http://localhost"),
+               new ClientSecretBasic(new ClientID("s6BhdRkqt3"), new Secret("foo")),
+               new AuthorizationCodeGrant(new AuthorizationCode("foo"), new URI("http://localhost")),
+               null,
+               List.of(new URI("https://sp3.example.org")),
+               null);
+       setTokenRequest(req);
+       
+       final AuthorizeCodeClaimsSet claims = new AuthorizeCodeClaimsSet.Builder()
+               .setJWTID(idGenerator)
+               .setClientID(new ClientID("s6BhdRkqt3"))
+               .setIssuer("issuer")
+               .setPrincipal("userPrin")
+               .setSubject("subject")
+               .setIssuedAt(Instant.now())
+               .setExpiresAt(Instant.now())
+               .setAuthenticationTime(Instant.now())
+               .setRedirectURI(new URI("http://localhost"))
+               .setScope(Scope.parse("openid email"))
+               .setAudience(List.of("https://sp3.example.org"))
+               .setACR(new ACR("0"))
+               .build();
+       respCtx.setAuthorizationGrantClaimsSet(claims);
+       
+       final Event event = action.execute(requestCtx);
+       ActionTestingSupport.assertEvent(event, EventIds.ACCESS_DENIED);
+   }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
index 5f4937fd..2ca7aed5 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
@@ -204,6 +204,7 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
         metadata.setTokenEndpointAuthJWSAlg(tokenEndpointSigAlg);
         metadata.setTokenEndpointAuthMethod(tokenEndpointMethod);
         metadata.setUserInfoJWSAlg(userInfoSigAlg);
+        metadata.setCustomField("audience", List.of("https://rp.example.org", "https://rp2.example.org"));
         final OIDCClientInformation information;
         if (publicKey == null) {
             information = new OIDCClientInformation(new ClientID(clientId), new Date(),
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/ClientCredentialsTokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/ClientCredentialsTokenFlowTest.java
index ecd09153..b109adfc 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/ClientCredentialsTokenFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/ClientCredentialsTokenFlowTest.java
@@ -42,11 +42,9 @@ import org.testng.annotations.AfterMethod;
 import org.testng.annotations.Test;
 
 import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.AccessTokenResponse;
-import com.nimbusds.oauth2.sdk.AuthorizationCode;
 import com.nimbusds.oauth2.sdk.GrantType;
 import com.nimbusds.oauth2.sdk.OAuth2Error;
 import com.nimbusds.oauth2.sdk.Scope;
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java
index 3c94ef42..ca9944d8 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java
@@ -39,6 +39,7 @@ import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrate
 import net.shibboleth.utilities.java.support.security.impl.BasicKeystoreKeyStrategy;
 import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifierGenerationStrategy;
 
+import org.opensaml.core.testing.OpenSAMLInitBaseTestCase;
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.security.credential.Credential;
@@ -68,7 +69,7 @@ import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 import net.shibboleth.ext.spring.resource.ResourceHelper;
 
 /** base class for tests expecting to have inbound and outbound msg ctxs etc in place. */
-public abstract class BaseOIDCResponseActionTest {
+public abstract class BaseOIDCResponseActionTest extends OpenSAMLInitBaseTestCase {
 
     protected RequestContext requestCtx;
 
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/EncryptProcessedTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/EncryptProcessedTokenTest.java
index 6d7a81d1..c30441b3 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/EncryptProcessedTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/EncryptProcessedTokenTest.java
@@ -20,7 +20,6 @@ package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 import net.minidev.json.JSONObject;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
-import net.shibboleth.idp.plugin.oidc.op.profile.impl.EncryptProcessedToken;
 import net.shibboleth.idp.profile.context.RelyingPartyContext;
 import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
@@ -170,13 +169,13 @@ public class EncryptProcessedTokenTest {
     }
 
     /**
-     * Test fail case of nothing to encrypt.
+     * Test case of nothing to encrypt.
      */
     @Test
-    public void testFailNoInput() {
+    public void testNoInput() {
         oidcRespCtx.setProcessedToken(null);
         final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, EventIds.UNABLE_TO_ENCRYPT);
+        ActionTestingSupport.assertProceedEvent(event);
     }
 
     /**
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCEncryptionParametersTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCEncryptionParametersTest.java
index 87410b17..967f2329 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCEncryptionParametersTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/PopulateOIDCEncryptionParametersTest.java
@@ -17,7 +17,6 @@
 
 package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 
-import net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCEncryptionParameters;
 import net.shibboleth.idp.profile.context.RelyingPartyContext;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
@@ -31,11 +30,12 @@ import org.opensaml.profile.action.EventIds;
 import org.opensaml.saml.saml2.profile.context.EncryptionContext;
 import org.opensaml.xmlsec.EncryptionParameters;
 import org.opensaml.xmlsec.EncryptionParametersResolver;
-import org.opensaml.xmlsec.criterion.EncryptionOptionalCriterion;
 import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import com.google.common.base.Predicates;
+
 /** {@link PopulateOIDCEncryptionParameters} unit test. */
 public class PopulateOIDCEncryptionParametersTest extends BaseOIDCResponseActionTest {
 
@@ -82,7 +82,11 @@ public class PopulateOIDCEncryptionParametersTest extends BaseOIDCResponseAction
     @Test
     public void testSuccessFailsToResolve() throws NoSuchAlgorithmException, ComponentInitializationException {
         resolver.resolve = false;
-        resolver.optional = true;
+
+        action = new PopulateOIDCEncryptionParameters();
+        action.setEncryptionOptionalPredicate(Predicates.alwaysTrue());
+        action.setEncryptionParametersResolver(resolver);
+        action.initialize();
 
         ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
         Assert.assertNull(profileRequestCtx.getSubcontext(RelyingPartyContext.class)
@@ -95,6 +99,11 @@ public class PopulateOIDCEncryptionParametersTest extends BaseOIDCResponseAction
     @Test
     public void testFailureFailsToResolve() throws NoSuchAlgorithmException, ComponentInitializationException {
         resolver.resolve = false;
+        
+        action = new PopulateOIDCEncryptionParameters();
+        action.setEncryptionOptionalPredicate(Predicates.alwaysFalse());
+        action.setEncryptionParametersResolver(resolver);
+        action.initialize();
         ActionTestingSupport.assertEvent(action.execute(requestCtx), EventIds.INVALID_SEC_CFG);
     }
 
@@ -122,7 +131,7 @@ public class PopulateOIDCEncryptionParametersTest extends BaseOIDCResponseAction
     @Test(expectedExceptions = ConstraintViolationException.class)
     public void testFailureNullStrategyMetadataContext() {
         action = new PopulateOIDCEncryptionParameters();
-        action.setOIDCMetadataContextContextLookupStrategy(null);
+        action.setOidcMetadataContextLookupStrategy(null);
     }
 
     /**
@@ -138,8 +147,6 @@ public class PopulateOIDCEncryptionParametersTest extends BaseOIDCResponseAction
 
         public boolean resolve = true;
 
-        public boolean optional;
-
         @Override
         public Iterable<EncryptionParameters> resolve(CriteriaSet criteria) throws ResolverException {
             return null;
@@ -147,9 +154,6 @@ public class PopulateOIDCEncryptionParametersTest extends BaseOIDCResponseAction
 
         @Override
         public EncryptionParameters resolveSingle(CriteriaSet criteria) throws ResolverException {
-            if (optional) {
-                criteria.add(new EncryptionOptionalCriterion(true));
-            }
             return resolve ? new EncryptionParameters() : null;
         }
 
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScopeTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScopeTest.java
index 301245ed..d322c947 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScopeTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScopeTest.java
@@ -132,7 +132,7 @@ public class ValidateScopeTest extends BaseOIDCResponseActionTest {
     public void testTokenClientCredentials() throws ComponentInitializationException, URISyntaxException {
 
         action = new ValidateScope();
-        action.setRequestedScopesLookupStrategy(new TokenRequestScopeLookupFunction());
+        action.setRequestedScopeLookupStrategy(new TokenRequestScopeLookupFunction());
         action.initialize();
         
         final TokenRequest req = new TokenRequest(new URI("http://localhost"),
@@ -166,7 +166,7 @@ public class ValidateScopeTest extends BaseOIDCResponseActionTest {
    public void testTokenNoGrantedScopes() throws ComponentInitializationException, URISyntaxException {
 
        action = new ValidateScope();
-       action.setRequestedScopesLookupStrategy(new TokenRequestScopeLookupFunction());
+       action.setRequestedScopeLookupStrategy(new TokenRequestScopeLookupFunction());
        action.initialize();
        
        final TokenRequest req = new TokenRequest(new URI("http://localhost"),
@@ -209,7 +209,7 @@ public class ValidateScopeTest extends BaseOIDCResponseActionTest {
    public void testTokenGrantedScopes() throws ComponentInitializationException, URISyntaxException {
 
        action = new ValidateScope();
-       action.setRequestedScopesLookupStrategy(new TokenRequestScopeLookupFunction());
+       action.setRequestedScopeLookupStrategy(new TokenRequestScopeLookupFunction());
        action.initialize();
        
        final TokenRequest req = new TokenRequest(new URI("http://localhost"),
@@ -258,7 +258,7 @@ public class ValidateScopeTest extends BaseOIDCResponseActionTest {
   public void testUserInfoGrantedScopes() throws ComponentInitializationException, URISyntaxException {
 
       action = new ValidateScope();
-      action.setRequestedScopesLookupStrategy(null);
+      action.setRequestedScopeLookupStrategy(null);
       action.initialize();
       
       final UserInfoRequest req =
diff --git a/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml b/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml
index d93be7c9..a9301b28 100644
--- a/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml
+++ b/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml
@@ -76,31 +76,6 @@
         <AttributeEncoder xsi:type="oidc:OIDCString" name="sub" />
     </AttributeDefinition>
 
-    <!-- Used to drive client_credentials grant handling. -->
-    
-    <AttributeDefinition id="requestedScope" xsi:type="ScriptedAttribute" resolutionPhases="oidc/token">
-        <Script>
-          <![CDATA[
-             requestedScope.getValues().addAll(
-               resolutionContext.getSubcontext(
-                 "net.shibboleth.idp.plugin.oidc.op.profile.context.OAuthAttributeResolutionContext").getScope()
-               );
-          ]]>
-        </Script>
-    </AttributeDefinition>
-
-    <AttributeDefinition id="requestedAudience" xsi:type="ScriptedAttribute" resolutionPhases="oidc/token">
-        <Script>
-          <![CDATA[
-             requestedAudience.getValues().addAll(
-               resolutionContext.getSubcontext(
-                 "net.shibboleth.idp.plugin.oidc.op.profile.context.OAuthAttributeResolutionContext").getResources()
-               );
-          ]]>
-        </Script>
-    </AttributeDefinition>
-
-
     <!-- ========================================== -->
     <!--      Data Connectors                       -->
     <!-- ========================================== -->
diff --git a/idp-oidc-extension-impl/src/test/resources/conf/oidc.properties b/idp-oidc-extension-impl/src/test/resources/conf/oidc.properties
index 557b62fd..0163cbbe 100644
--- a/idp-oidc-extension-impl/src/test/resources/conf/oidc.properties
+++ b/idp-oidc-extension-impl/src/test/resources/conf/oidc.properties
@@ -7,6 +7,3 @@ idp.oidc.subject.salt = isfd07fsddfs70sdf9d99s8
 idp.oidc.discovery.template = src/test/resources/conf/openid-configuration.json
 
 idp.oidc.dynreg.defaultMetadataPolicyFile = src/test/resources/conf/metadata-policy1.json
-
-idp.oauth.accessToken.scopeAttribute = requestedScope
-idp.oauth.accessToken.audienceAttribute = requestedAudience
diff --git a/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
index d9c39df4..13f0b1e0 100644
--- a/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
@@ -44,7 +44,7 @@
                 <ref bean="SAML2.AttributeQuery.MDDriven" />
                 <ref bean="SAML2.ArtifactResolution.MDDriven" />
                 <ref bean="OIDC.SSO.MDDriven" />
-                <ref bean="OIDC.Token.MDDriven" />
+                <bean parent="OIDC.Token.MDDriven" p:allowUntrustedAudience="true" />
                 <ref bean="OIDC.UserInfo.MDDriven" />
                 <ref bean="OIDC.Registration.MDDriven" />
                 <ref bean="OAUTH2.Revocation.MDDriven" />
@@ -81,7 +81,7 @@
         <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdJWT">
             <property name="profileConfigurations">
                  <list>
-                     <bean parent="OIDC.Token.MDDriven" p:accessTokenType="JWT" />
+                     <bean parent="OIDC.Token.MDDriven" p:accessTokenType="JWT" p:allowUntrustedAudience="true" />
                  </list>
             </property>
         </bean>

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list