[java-idp-oidc] branch main updated: JOIDC-7 - Support JWT access tokens for code or implicit grants

Scott Cantor cantor.2 at osu.edu
Wed Apr 27 14:30:37 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=4fd9d227f58b9be2fd3b5a82758c674d321785ab

The following commit(s) were added to refs/heads/main by this push:
     new 4fd9d227 JOIDC-7 - Support JWT access tokens for code or implicit grants
4fd9d227 is described below

commit 4fd9d227f58b9be2fd3b5a82758c674d321785ab
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Apr 27 10:29:20 2022 -0400

    JOIDC-7 - Support JWT access tokens for code or implicit grants
    
    https://shibboleth.atlassian.net/browse/JOIDC-7
    
    Working patch for code and implicit.
    Also removed redirect_uri claim from access tokens.
---
 .../AbstractTokenClaimsLookupFunction.java         |   2 +-
 .../navigate/ClientInfoAudienceLookupFunction.java |   3 +
 .../DefaultRequestAudienceLookupFunction.java      |  69 ++++
 .../DefaultResponseClaimsSetLookupFunction.java    |   2 +-
 ...uthenticationResponseContextLookupFunction.java |   4 +-
 .../TokenRequestRedirectURILookupFunction.java     |   2 +-
 .../op/token/support/AccessTokenClaimsSet.java     |  48 ++-
 .../op/token/support/RefreshTokenClaimsSet.java    |   2 +-
 .../oidc/op/token/support/TokenClaimsSet.java      |   3 +
 .../op/oauth2/profile/impl/BuildAccessToken.java   | 320 ++++++++++++++---
 .../op/oauth2/profile/impl/ValidateAudience.java   |  63 +++-
 .../impl/AbstractOIDCTokenResponseAction.java      |   2 +-
 .../profile/impl/AddAccessTokenHashToIDToken.java  |   3 +-
 .../op/profile/impl/AddAttributesToClaimsSet.java  |   1 -
 .../impl/SetAccessTokenToResponseContext.java      | 332 ------------------
 .../SetAuthorizationCodeToResponseContext.java     |  21 +-
 ...liveryAttributesFromTokenToResponseContext.java |   2 +-
 .../plugin/oidc/op/profile/impl/ValidateGrant.java |   6 +-
 .../oidc/op/profile/impl/ValidateRedirectURI.java  |   2 +-
 .../plugin/oidc/op/profile/impl/ValidateScope.java |   6 +-
 ...uteConsentEnabledInTokenClaimsSetPredicate.java |   2 +-
 .../impl/BuildUserInfoErrorResponseFromEvent.java  |   4 +-
 .../impl/FormOutboundUserInfoResponseMessage.java  |   3 +-
 ...lizeOutboundUserInfoResponseMessageContext.java |   4 +-
 .../op/userinfo/profile/impl/ParseAccessToken.java | 203 +++++++++++
 .../profile/impl/SignUserInfoResponse.java         |   3 +-
 .../userinfo/profile/impl/ValidateAccessToken.java | 116 +++----
 .../META-INF/net.shibboleth.idp/postconfig.xml     |   1 +
 .../idp/flows/oidc/authorize/authorize-beans.xml   | 385 +++++++++++++++------
 .../idp/flows/oidc/authorize/authorize-flow.xml    | 141 +++++++-
 .../idp/flows/oidc/token/token-beans.xml           | 163 +++++----
 .../shibboleth/idp/flows/oidc/token/token-flow.xml | 107 ++++--
 .../idp/flows/oidc/userinfo/userinfo-beans.xml     |  28 +-
 .../idp/flows/oidc/userinfo/userinfo-flow.xml      |   3 +-
 .../idp/service/relying-party/postconfig.xml       |  46 ++-
 .../oauth2/profile/impl/BuildAccessTokenTest.java  |  13 +-
 .../oidc/op/profile/flow/AbstractOidcFlowTest.java |   2 +-
 .../oidc/op/profile/flow/AuthorizeFlowTest.java    | 219 +++++++++---
 .../plugin/oidc/op/profile/flow/TokenFlowTest.java |   7 +-
 .../profile/impl/BaseOIDCResponseActionTest.java   |  10 +-
 .../impl/SetAccessTokenToResponseContextTest.java  | 283 ---------------
 .../FormOutboundUserInfoResponseMessageTest.java   |   3 +-
 ...essTokenTest.java => ParseAccessTokenTest.java} | 190 +++++-----
 .../profile/impl/SignUserInfoResponseTest.java     |   3 +-
 .../profile/impl/ValidateAccessTokenTest.java      | 138 +++-----
 pom.xml                                            |   4 +-
 46 files changed, 1731 insertions(+), 1243 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractTokenClaimsLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractTokenClaimsLookupFunction.java
index f74cc2e9..94dc3e1e 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractTokenClaimsLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractTokenClaimsLookupFunction.java
@@ -50,7 +50,7 @@ public abstract class AbstractTokenClaimsLookupFunction<T>
             return null;
         }
         final OIDCAuthenticationResponseContext oidcResponseContext =
-                input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class, false);
+                input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
         if (oidcResponseContext == null) {
             return null;
         }
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
index 9ae72310..e4170897 100644
--- 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
@@ -20,6 +20,7 @@ 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.Collections;
 import java.util.List;
 
 import javax.annotation.Nonnull;
@@ -70,6 +71,8 @@ public class ClientInfoAudienceLookupFunction implements ContextDataLookupFuncti
             }
             
             return audience;
+        } else if (obj instanceof String) {
+            return Collections.singletonList((String) obj);
         }
         
         return null;
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultRequestAudienceLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultRequestAudienceLookupFunction.java
new file mode 100644
index 00000000..b8b72b50
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultRequestAudienceLookupFunction.java
@@ -0,0 +1,69 @@
+/*
+ * 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.text.ParseException;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
+
+/**
+ * A function that returns resource value of the authentication request.
+ * 
+ * @since 3.2.0
+ */
+public class DefaultRequestAudienceLookupFunction extends AbstractAuthenticationRequestLookupFunction<List<String>> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultRequestAudienceLookupFunction.class);
+
+    /** {@inheritDoc} */
+    @Nullable protected List<String> doLookup(@Nonnull final AuthenticationRequest req) {
+        try {
+            if (getRequestObject() != null && getRequestObject().getJWTClaimsSet().getClaim("resource") != null) {
+                final Object resource = getRequestObject().getJWTClaimsSet().getClaim("resource");
+                if (resource instanceof String) {
+                    return Collections.singletonList((String) resource);
+                } else if (resource instanceof List) {
+                    return (List<String>) resource;
+                } else if (resource != null) {
+                    log.error("resource claim is not of expected type");
+                    return null;
+                }
+
+            }
+        } catch (final ParseException e) {
+            log.error("Unable to parse request object");
+            return null;
+        }
+        
+        return req.getResources() == null ? null :
+            req.getResources().stream()
+                .map(URI::toString)
+                .collect(Collectors.toUnmodifiableList());
+    }
+
+}
\ 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/DefaultResponseClaimsSetLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultResponseClaimsSetLookupFunction.java
index 3d00b22b..92fd6c03 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultResponseClaimsSetLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultResponseClaimsSetLookupFunction.java
@@ -36,7 +36,7 @@ public class DefaultResponseClaimsSetLookupFunction
             return null;
         }
         final OIDCAuthenticationResponseContext ctx =
-                input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class, false);
+                input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
         if (ctx == null) {
             return null;
         }
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/OIDCAuthenticationResponseContextLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/OIDCAuthenticationResponseContextLookupFunction.java
index 13eb87c3..b5ec3ed5 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/OIDCAuthenticationResponseContextLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/OIDCAuthenticationResponseContextLookupFunction.java
@@ -29,7 +29,7 @@ import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationRes
  * {@link ProfileRequestContext}.
  */
 public class OIDCAuthenticationResponseContextLookupFunction
-        implements ContextDataLookupFunction<ProfileRequestContext, OIDCAuthenticationResponseContext> {
+        implements ContextDataLookupFunction<ProfileRequestContext,OIDCAuthenticationResponseContext> {
 
     /** {@inheritDoc} */
     @Nullable
@@ -37,7 +37,7 @@ public class OIDCAuthenticationResponseContextLookupFunction
         if (input == null || input.getOutboundMessageContext() == null) {
             return null;
         }
-        return input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class, false);
+        return input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
     }
 
 }
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestRedirectURILookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestRedirectURILookupFunction.java
index 81f4ce6d..1b5b1015 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestRedirectURILookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestRedirectURILookupFunction.java
@@ -57,7 +57,7 @@ public class TokenRequestRedirectURILookupFunction extends AbstractTokenRequestL
         try {
             uri = new URI(redirectURI);
         } catch (final URISyntaxException e) {
-            log.warn("Unable to parse uri from token request redirect_uri {}", redirectURI);
+            log.warn("Unable to parse URI from token request redirect_uri {}", redirectURI);
         }
         return uri;
     }
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
index e3ac61c4..0bce7967 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
@@ -20,6 +20,7 @@ package net.shibboleth.idp.plugin.oidc.op.token.support;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.id.ClientID;
@@ -34,6 +35,7 @@ import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrate
 import java.net.URI;
 import java.text.ParseException;
 import java.time.Instant;
+import java.util.Map;
 
 /** Class wrapping claims set for access token. */
 public final class AccessTokenClaimsSet extends TokenClaimsSet {
@@ -63,9 +65,9 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
         setClaimsSet(builder.buildJWTClaimsSet(VALUE_TYPE_AT));
     }
 // Checkstyle: ParameterNumber ON
-
+    
     /**
-     * Private constructor for the parser.
+     * Direct constructor.
      * 
      * @param accessTokenClaimsSet access token claims set
      */
@@ -102,6 +104,44 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
         return parse(dataSealer.unwrap(wrappedAccessToken));
     }
 
+    /**
+     * Parses access token from sealed access token.
+     * 
+     * @param jwtAccessToken wrapped access token
+     * @param dataSealer sealer to unwrap the access token
+     * 
+     * @return access token claims set
+     * 
+     * @throws ParseException is thrown if unwrapped access token is not understood
+     * @throws DataSealerException is thrown if unwrapping fails
+     * 
+     * @since 3.2.0
+     */
+    @Nonnull public static AccessTokenClaimsSet parse(@Nonnull @NotEmpty final JWT jwtAccessToken,
+            @Nonnull final DataSealer dataSealer) throws ParseException, DataSealerException {
+        
+        JWTClaimsSet claims = jwtAccessToken.getJWTClaimsSet();
+        
+        // Check for embedded custom claim.
+        if (claims.getClaim(TokenClaimsSet.KEY_SEALED_FOR_OP) == null) {
+            // Throws exception if parsing result is not expected one.
+            verifyParsedClaims(VALUE_TYPE_AT, claims);
+            return new AccessTokenClaimsSet(claims);
+        }
+        
+        final Map<String,Object> map = claims.toJSONObject();
+        final JWTClaimsSet unsealed = JWTClaimsSet.parse(
+                dataSealer.unwrap(claims.getStringClaim(TokenClaimsSet.KEY_SEALED_FOR_OP)));
+        map.remove(TokenClaimsSet.KEY_SEALED_FOR_OP);
+        for (Map.Entry<String,Object> claim : unsealed.getClaims().entrySet()) {
+            map.put(claim.getKey(), claim.getValue());
+        }
+        
+        claims = JWTClaimsSet.parse(map);
+        verifyParsedClaims(VALUE_TYPE_AT, claims);
+        return new AccessTokenClaimsSet(claims);
+    }
+    
     /** Builder to create instance of AccessTokenClaimsSet. */
     public static final class Builder extends TokenClaimsSet.Builder<AccessTokenClaimsSet> {
 
@@ -144,7 +184,6 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
             setIssuedAt(issuedAt);
             setExpiresAt(expiresAt);
             setAuthenticationTime(authenticationTime);
-            setRedirectURI(redirectURI);
             setScope(scope);
         }
         
@@ -188,7 +227,6 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
             setNotBefore(existing.getNotBefore());
             setAuthenticationTime(existing.getAuthenticationTime());
             setAudience(existing.getAudience());
-            setRedirectURI(existing.getRedirectURI());
             setClaimsRequest(existing.getClaimsRequest());
             setConsentedClaims(existing.getConsentedClaims());
             setConsentEnabled(existing.isConsentEnabled());
@@ -204,7 +242,7 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
         public AccessTokenClaimsSet build() {
             return new AccessTokenClaimsSet(buildJWTClaimsSet(VALUE_TYPE_AT));
         }
-        
+                
     }
 
 }
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java
index 1bfbbf3b..682363e9 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java
@@ -136,8 +136,8 @@ public final class RefreshTokenClaimsSet extends TokenClaimsSet {
             setPrincipal(existing.getPrincipal());
             setSubject(existing.getClaimsSet().getSubject());
             setACR(existing.getACR() == null ? null : new ACR(existing.getACR()));
-            setNonce(existing.getNonce());
             setAuthenticationTime(existing.getAuthenticationTime());
+            setNonce(existing.getNonce());
             setRedirectURI(existing.getRedirectURI());
             setScope(existing.getScope());
             setClaimsRequest(existing.getClaimsRequest());
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
index 0c3b1790..ce81beb2 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
@@ -130,6 +130,9 @@ public class TokenClaimsSet {
     /** Code Challenge. */
     @Nonnull @NotEmpty public static final String KEY_CODE_CHALLENGE = "cc";
 
+    /** Custom claim name for sealed claims embedded inside JWT. */
+    @Nonnull @NotEmpty public static final String KEY_SEALED_FOR_OP = "for_op";
+    
     /** Claims set for the claim. */
     @Nullable private JWTClaimsSet tokenClaimsSet;
 
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 ba01d36c..55a783ed 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,9 +17,13 @@
 
 package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
 
+import java.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -31,25 +35,38 @@ import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.PlainJWT;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
 
-import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.minidev.json.JSONArray;
+import net.shibboleth.idp.authn.context.SubjectContext;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.AccessTokenContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseConsentContext;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseTokenClaimsContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.OIDCAuthenticationResponseContextLookupFunction;
 import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestClientIDLookupFunction;
 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.AuthorizeCodeClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet.Builder;
 import net.shibboleth.idp.profile.IdPEventIds;
 import net.shibboleth.idp.profile.context.navigate.ResponderIdLookupFunction;
+import net.shibboleth.oidc.profile.config.logic.AttributeConsentFlowEnabledPredicate;
 import net.shibboleth.oidc.profile.config.navigate.AccessTokenLifetimeLookupFunction;
 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.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.logic.FunctionSupport;
@@ -60,12 +77,13 @@ import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifie
 
 /**
  * Action that creates an Access Token, and stores it to an {@link AccessTokenContext}.
+ *
+ * <p>There are various cases handled across different grant types and orders of operation.
+ * The token may be produced solely for a third-party service to consume, or may also or instead
+ * be usable with the OP's UserInfo endpoint.</p>
  * 
- * <p>This supports arbitrary ("generic") OAuth access tokens requested by the "client_credentials" grant type.
- * This is a "pure" OAuth use case and does not involve any OIDC behavior.</p>
- * 
- * <p>The action supports either fully opaque access tokens sealed under the IdP's secret key, or
- * the RFC 9068 standard for JWT-based tokens.</p>
+ * <p>The action supports either opaque access tokens sealed under the IdP's secret key, or the
+ * RFC 9068 standard for JWT-based tokens.</p>
  * 
  * @event {@link EventIds#PROCEED_EVENT_ID}
  * @event {@link EventIds#MESSAGE_PROC_ERROR}
@@ -84,7 +102,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
     @Nonnull private Logger log = LoggerFactory.getLogger(BuildAccessToken.class);
 
     /** Sealer to use for opaque tokens. */
-    @Nullable private DataSealer dataSealer;
+    @NonnullAfterInit private DataSealer dataSealer;
     
     /** Strategy used to obtain the response issuer value. */
     @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
@@ -101,21 +119,38 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
     /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
     @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
 
+    /** Strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext}. */
+    @Nonnull private Function<ProfileRequestContext,OIDCAuthenticationResponseTokenClaimsContext>
+        tokenClaimsContextLookupStrategy;
+    
+    /** Strategy used to locate the {@link OIDCAuthenticationResponseConsentContext}. */
+    @Nonnull private Function<ProfileRequestContext, OIDCAuthenticationResponseConsentContext>
+        consentContextLookupStrategy;
+    
+    /** Predicate used to check if consent is enabled with a given {@link ProfileRequestContext}. */
+    @Nonnull private Predicate<ProfileRequestContext> consentEnabledPredicate;
+    
     /** Strategy used to create the subcontext to hold the token. */
     @Nonnull private Function<ProfileRequestContext,AccessTokenContext> accessTokenContextCreationStrategy;
+
+    /** Authorize Code / Refresh Token the access token is based on, if any. */
+    @Nullable private TokenClaimsSet tokenClaimsSet;
+
+    /** Authentication request in the case of such. */
+    @Nullable private AuthenticationRequest authenticationRequest;
+    
+    /** Subject context. */
+    @Nullable private SubjectContext subjectCtx;
     
     /** Use a JWT for the token. */
     private boolean jwtTokenType;
     
-    /** Access token context. */
-    @Nullable private AccessTokenContext accessTokenCtx;
-    
-    /** Attribute context. */
-    @Nullable private AttributeContext attributeCtx;
-
     /** The generator to use. */
     @Nullable private IdentifierGenerationStrategy idGenerator;
     
+    /** Access token context. */
+    @Nullable private AccessTokenContext accessTokenCtx;
+
     /** Constructor. */
     public BuildAccessToken() {
         accessTokenTypeLookupStrategy = new AccessTokenTypeLookupFunction();
@@ -127,6 +162,15 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         
         idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
         
+        tokenClaimsContextLookupStrategy =
+                new ChildContextLookup<>(OIDCAuthenticationResponseTokenClaimsContext.class).compose(
+                        new OIDCAuthenticationResponseContextLookupFunction());
+        consentContextLookupStrategy =
+                new ChildContextLookup<>(OIDCAuthenticationResponseConsentContext.class).compose(
+                        new OIDCAuthenticationResponseContextLookupFunction());
+
+        consentEnabledPredicate = new AttributeConsentFlowEnabledPredicate();
+        
         // PRC -> inbound message context -> OIDC response context -> ATC
         accessTokenContextCreationStrategy = new ChildContextLookup<>(AccessTokenContext.class, true).compose(
                 new ChildContextLookup<>(OIDCAuthenticationResponseContext.class).compose(
@@ -182,38 +226,86 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
     }
     
     /**
-     * Set the strategy used to create the {@link AccessTokenContext} to use.
+     * Set the strategy used to locate the issuer value to use.
      * 
-     * @param strategy creation strategy
+     * @param strategy lookup strategy
      */
-    public void setAccessTokenContextCreationStrategy(
-            @Nonnull final Function<ProfileRequestContext,AccessTokenContext> strategy) {
+    public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
-        accessTokenContextCreationStrategy =
-                Constraint.isNotNull(strategy, "AccessTokenContext creation strategy cannot be null");
+        issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
     }
     
     /**
-     * Set the strategy used to locate the issuer value to use.
+     * Set the strategy used to locate the original {@link ClientID} from the request.
      * 
      * @param strategy lookup strategy
      */
-    public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+    public void setClientIDLookupStrategy(@Nonnull final Function<ProfileRequestContext,ClientID> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
-        issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
+        clientIDLookupStrategy = Constraint.isNotNull(strategy, "ClientID lookup strategy cannot be null");
     }
     
     /**
-     * Set the strategy used to locate the original {@link ClientID} from the request.
+     * Set the strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext} associated with a given
+     * {@link ProfileRequestContext}.
      * 
      * @param strategy lookup strategy
      */
-    public void setClientIDLookupStrategy(@Nonnull final Function<ProfileRequestContext,ClientID> strategy) {
+    public void setOIDCAuthenticationResponseTokenClaimsContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,OIDCAuthenticationResponseTokenClaimsContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        tokenClaimsContextLookupStrategy = Constraint.isNotNull(strategy,
+                "OIDCAuthenticationResponseTokenClaimsContextt lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the {@link OIDCAuthenticationResponseConsentContext} associated with a given
+     * {@link ProfileRequestContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setOIDCAuthenticationResponseConsentContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,OIDCAuthenticationResponseConsentContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        consentContextLookupStrategy = Constraint.isNotNull(strategy,
+                "OIDCAuthenticationResponseConsentContext lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the predicate used to check if consent is enabled with a given {@link ProfileRequestContext}.
+     * 
+     * @param predicate predicate used to check if consent is enabled with a given {@link ProfileRequestContext}.
+     */
+    public void setConsentEnabledPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        consentEnabledPredicate =
+                Constraint.isNotNull(predicate, "predicate used to check if consent is enabled cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to create the {@link AccessTokenContext} to use.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setAccessTokenContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,AccessTokenContext> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
-        clientIDLookupStrategy = Constraint.isNotNull(strategy, "ClientID lookup strategy cannot be null");
+        accessTokenContextCreationStrategy =
+                Constraint.isNotNull(strategy, "AccessTokenContext creation strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (dataSealer == null) {
+            throw new ComponentInitializationException("DataSealer cannot be null");
+        }
     }
 
     /** {@inheritDoc} */
@@ -232,13 +324,42 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
             return false;
         }
         
-        idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
-        if (idGenerator == null) {
-            log.error("{} No identifier generation strategy", getLogPrefix());
+        tokenClaimsSet = getOidcResponseContext().getAuthorizationGrantClaimsSet();
+        if (tokenClaimsSet != null && !(tokenClaimsSet instanceof RefreshTokenClaimsSet)
+                && !(tokenClaimsSet instanceof AuthorizeCodeClaimsSet)) {
+            log.error("{} Authorization grant is of unknown type: {}", getLogPrefix(), tokenClaimsSet.getClass().getName());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return false;
         }
-
+        
+        if (tokenClaimsSet == null) {
+            /*
+             * Typically this path applies when the client_credentials grant is used.
+             * 
+             * Alternatively the access token may be provided by the authz endpoint without a code.
+             * This is the case only with the "token id_token" response type.
+             */
+            subjectCtx = profileRequestContext.getSubcontext(SubjectContext.class);
+            if (subjectCtx == null) {
+                log.error("{} No subject context", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+                return false;
+            }
+            
+            idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
+            if (idGenerator == null) {
+                log.error("{} No identifier generation strategy", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+                return false;
+            }
+            
+            if (profileRequestContext.getInboundMessageContext() != null
+                    && profileRequestContext.getInboundMessageContext().getMessage() instanceof AuthenticationRequest) {
+                authenticationRequest =
+                        (AuthenticationRequest) profileRequestContext.getInboundMessageContext().getMessage();
+            }
+        }
+        
         accessTokenCtx = accessTokenContextCreationStrategy.apply(profileRequestContext);
         if (accessTokenCtx == null) {
             log.error("{} Unable to create AccessTokenContext", getLogPrefix());
@@ -268,27 +389,71 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
             return;
         }
         
+        ClaimsSet claims = null;
+        ClaimsSet claimsUI = null;
+        final OIDCAuthenticationResponseTokenClaimsContext tokenClaimsCtx =
+                tokenClaimsContextLookupStrategy.apply(profileRequestContext);
+        if (tokenClaimsCtx != null) {
+            claims = tokenClaimsCtx.getClaims();
+            claimsUI = tokenClaimsCtx.getUserinfoClaims();
+        }
+        
         final OIDCAuthenticationResponseContext responseCtx = getOidcResponseContext();
 
         final Scope scope = responseCtx.getScope() != null ? responseCtx.getScope() : new Scope();
+        log.debug("{} Building access token with scope: {}", getLogPrefix(), scope);
+        
+        final boolean oidc = scope.contains("openid");
         
+        if (oidc) {
+            responseCtx.getAudience().add(issuer);
+        }
         log.debug("{} Building access token with audience: {}", getLogPrefix(), responseCtx.getAudience());
-        log.debug("{} Building access token with scope: {}", getLogPrefix(), scope);
 
         final Instant now = Instant.now();
         final Instant dateExp = now.plus(accessTokenCtx.getLifetime());
         
-        final AccessTokenClaimsSet.Builder builder = (Builder) new AccessTokenClaimsSet.Builder()
-                .setJWTID(idGenerator)
-                .setClientID(clientID)
-                .setIssuer(issuer)
-                .setSubject(responseCtx.getSubject())
-                .setIssuedAt(now)
-                .setExpiresAt(dateExp)
-                .setACR(responseCtx.getAcr())
-                .setAuthenticationTime(responseCtx.getAuthTime())
-                .setScope(scope)
-                .setAudience(responseCtx.getAudience());
+        final AccessTokenClaimsSet.Builder builder;
+        
+        if (tokenClaimsSet != null) {
+            // We may not use original claims as input for scope / delivery claims as they may have been reduced.
+            builder = new AccessTokenClaimsSet.Builder(
+                    tokenClaimsSet,
+                    scope,
+                    oidc ? claims : null,
+                    oidc ? claimsUI : null,
+                    Instant.now(),
+                    dateExp);
+            // Add additional bits.
+            builder.setAudience(responseCtx.getAudience());
+        } else {
+            final OIDCAuthenticationResponseConsentContext consentCtx =
+                    consentContextLookupStrategy.apply(profileRequestContext);
+            final JSONArray consented = consentCtx != null ? consentCtx.getConsentedAttributes() : null;
+            
+            builder = (Builder) new AccessTokenClaimsSet.Builder()
+                    .setJWTID(idGenerator)
+                    .setClientID(clientID)
+                    .setIssuer(issuer)
+                    .setPrincipal(subjectCtx.getPrincipalName())
+                    .setSubject(responseCtx.getSubject())
+                    .setIssuedAt(now)
+                    .setExpiresAt(dateExp)
+                    .setACR(responseCtx.getAcr())
+                    .setAuthenticationTime(responseCtx.getAuthTime())
+                    .setScope(scope)
+                    .setAudience(responseCtx.getAudience())
+                    .setDlClaims(claims)
+                    .setDlClaimsUI(claimsUI)
+                    .setConsentedClaims(consented)
+                    .setConsentEnabled(consentEnabledPredicate.test(profileRequestContext));
+
+            if (authenticationRequest != null) {
+                builder
+                    .setNonce(authenticationRequest.getNonce())
+                    .setClaimsRequest(authenticationRequest.getOIDCClaims());
+            }
+        }
         
         if (jwtTokenType && responseCtx.getAccessTokenClaimSet() != null) {
             builder.setCustomClaims(responseCtx.getAccessTokenClaimSet().toJSONObject());
@@ -298,18 +463,79 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         
         try {
             if (jwtTokenType) {
-                accessTokenCtx.setJWT(new PlainJWT(claimsSet.getClaimsSet()));
-                log.debug("{} Claims stored to JWT access token: {}", getLogPrefix(), claimsSet.serialize(),
-                        accessTokenCtx.getJWT());
+                accessTokenCtx.setJWT(new PlainJWT(sealClaims(claimsSet.getClaimsSet())));
+                log.debug("{} Claims stored to JWT access token: {}", getLogPrefix(), claimsSet.serialize());
             } else { 
                 accessTokenCtx.setOpaque(claimsSet.serialize(dataSealer));
-                log.debug("{} Claims '{}' converted to opaque access token: {}", getLogPrefix(), claimsSet.serialize(),
-                        accessTokenCtx.getOpaque());
+                log.debug("{} Claims converted to opaque access token: {}", getLogPrefix(), claimsSet.serialize());
             }
-        } catch (final DataSealerException e) {
+        } catch (final DataSealerException | ParseException e) {
             log.error("{} Access Token wrapping failed: {}", getLogPrefix(), e);
             ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
         }
     }
 
+    /**
+     * Rewrites a plaintext claimsset to hide custom claims used solely by the OP.
+     * 
+     * @param claims the input claims
+     * 
+     * @return a rewritten claims set to use for the access token
+     * 
+     * @throws ParseException if unable to parse a claims set
+     * @throws DataSealerException if unable to seal the custom claims
+     */
+    @Nonnull private JWTClaimsSet sealClaims(@Nonnull final JWTClaimsSet claims) throws DataSealerException, ParseException {
+        
+        // Rewrite as a mutable map.
+        final Map<String,Object> map = claims.toJSONObject();
+
+        // Put the claims to hide here.
+        final Map<String,Object> toSeal = new HashMap<>();
+
+        if (map.containsKey(TokenClaimsSet.KEY_TYPE)) {
+            toSeal.put(TokenClaimsSet.KEY_TYPE, map.remove(TokenClaimsSet.KEY_TYPE));
+        }
+        
+        if (map.containsKey(TokenClaimsSet.KEY_USER_PRINCIPAL)) {
+            toSeal.put(TokenClaimsSet.KEY_USER_PRINCIPAL, map.remove(TokenClaimsSet.KEY_USER_PRINCIPAL));
+        }
+        
+        if (map.containsKey(TokenClaimsSet.KEY_DELIVERY_CLAIMS)) {
+            toSeal.put(TokenClaimsSet.KEY_DELIVERY_CLAIMS, map.remove(TokenClaimsSet.KEY_DELIVERY_CLAIMS));
+        }
+
+        if (map.containsKey(TokenClaimsSet.KEY_DELIVERY_CLAIMS_USERINFO)) {
+            toSeal.put(TokenClaimsSet.KEY_DELIVERY_CLAIMS_USERINFO, map.remove(TokenClaimsSet.KEY_DELIVERY_CLAIMS_USERINFO));
+        }
+
+        if (map.containsKey(TokenClaimsSet.KEY_CONSENTED_CLAIMS)) {
+            toSeal.put(TokenClaimsSet.KEY_CONSENTED_CLAIMS, map.remove(TokenClaimsSet.KEY_CONSENTED_CLAIMS));
+        }
+
+        if (map.containsKey(TokenClaimsSet.KEY_CONSENT_ENABLED)) {
+            toSeal.put(TokenClaimsSet.KEY_CONSENT_ENABLED, map.remove(TokenClaimsSet.KEY_CONSENT_ENABLED));
+        }
+
+        if (map.containsKey(TokenClaimsSet.KEY_CODE_CHALLENGE)) {
+            toSeal.put(TokenClaimsSet.KEY_CODE_CHALLENGE, map.remove(TokenClaimsSet.KEY_CODE_CHALLENGE));
+        }
+
+        if (map.containsKey(TokenClaimsSet.KEY_NONCE)) {
+            toSeal.put(TokenClaimsSet.KEY_NONCE, map.remove(TokenClaimsSet.KEY_NONCE));
+        }
+
+        if (toSeal.isEmpty()) {
+            // Nothing to do.
+            return claims;
+        }
+        
+        // Wrap the sealed claims and re-embed back in original claims set.
+        final String sealed = dataSealer.wrap(JWTClaimsSet.parse(toSeal).toString());
+        map.put(TokenClaimsSet.KEY_SEALED_FOR_OP, sealed);
+        
+        // Re-parse the claims.
+        return JWTClaimsSet.parse(map);
+    }
+    
 }
\ 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/ValidateAudience.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudience.java
index 0b926b34..20ae0969 100644
--- 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
@@ -21,6 +21,7 @@ import java.util.ArrayList;
 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.profile.context.navigate.ClientInfoAudi
 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.plugin.oidc.op.profile.logic.IssueIDTokenCondition;
 import net.shibboleth.idp.profile.context.navigate.RelyingPartyIdLookupFunction;
 import net.shibboleth.oidc.profile.core.OidcEventIds;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
@@ -53,8 +55,9 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  * <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>
+ * <p>Requesting values is optional. If the OP is an implied audience, then no other audience will
+ * be established, but if not then at least one audience must be permitted and the first permitted
+ * value will be assumed.</p>
  * 
  * @event {@link EventIds#PROCEED_EVENT_ID}
  * @event {@link EventIds#INVALID_PROFILE_CTX}
@@ -77,6 +80,9 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
     /** Strategy used for locating/creating the proxy context. */
     @Nonnull private Function<ProfileRequestContext,ProxiedRequesterContext> proxiedRequesterContextCreationStrategy;
     
+    /** Whether the request includes the OP as an audience. */
+    @Nonnull private Predicate<ProfileRequestContext> selfAudienceCondition;
+    
     /** Strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext}. */
     @Nonnull
     private Function<ProfileRequestContext,OIDCAuthenticationResponseTokenClaimsContext>
@@ -90,6 +96,8 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
                 new DefaultOIDCMetadataContextLookupFunction());
         proxiedRequesterContextCreationStrategy = new ChildContextLookup<>(ProxiedRequesterContext.class, true).compose(
                 new OutboundMessageContextLookup());
+        // openid scope -> we're issuing an ID token -> the OP will be an audience for the access token
+        selfAudienceCondition = new IssueIDTokenCondition();
     }
 
     /**
@@ -126,7 +134,7 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
         allowedAudienceLookupStrategy = Constraint.isNotNull(strategy,
-                "Allowed scope lookyp strategy cannot be null");
+                "Allowed scope lookup strategy cannot be null");
     }
     
     /**
@@ -142,20 +150,29 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
                 "ProxiedRequesterContext lookup strategy cannot be null");
     }
     
+    /**
+     * Set whether the OP is an implied audience for the token request.
+     * 
+     * @param condition condition to set
+     * 
+     * @since 3.2.0
+     */
+    public void setSelfAudienceCondition(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        selfAudienceCondition = Constraint.isNotNull(condition, "Self audience condition cannot be null");
+    }
+    
 // Checkstyle: CyclomaticComplexity OFF
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         
         final String clientId = relyingPartyIdLookupStrategy.apply(profileRequestContext);
+        final boolean allowNone = selfAudienceCondition.test(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, OidcEventIds.INVALID_TARGET);
-            return;
-        }
         
         // These come from a previous authorization grant (authz code or refresh token).
         List<String> previouslyGrantedAudience = null;
@@ -166,7 +183,21 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
         // 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 (allowedAudience == null || allowedAudience.isEmpty()) {
+            if (allowNone) {
+                if (previouslyGrantedAudience != null || requestedAudience != null) {
+                    log.warn("{} No allowed audiences for client {}, OP will be sole audience", getLogPrefix(), clientId);
+                } else {
+                    log.debug("{} No allowed audiences for client {}, OP will be sole audience", getLogPrefix(), clientId);
+                }
+            } else {
+                log.warn("{} No allowed audience for client {}", getLogPrefix(), clientId);
+                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_TARGET);
+            }
+            return;
+        }
+
         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.
@@ -175,7 +206,11 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
         }
         
         if (requestedAudience == null) {
-            // Nothing requested or previously granted, so take the first allowed value. 
+            // Nothing requested or previously granted.
+            if (allowNone) {
+                log.debug("{} No audience in request for client {}, OP will be sole audience", getLogPrefix(), clientId);
+                return;
+            }
             log.debug("{} No audience in request for client {}, using first allowed", getLogPrefix(), clientId);
             requestedAudience = Collections.singletonList(allowedAudience.get(0));
         }
@@ -195,8 +230,12 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
         }
         
         if (effectiveAudience.isEmpty()) {
-            log.warn("{} No allowed audience for client {}", getLogPrefix(), clientId);
-            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_TARGET);
+            if (allowNone) {
+                log.debug("{} No allowed audience for client {}, OP will be sole audience", getLogPrefix(), clientId);
+            } else {
+                log.warn("{} No allowed audience for client {}", getLogPrefix(), clientId);
+                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_TARGET);
+            }
             return;
         }
         
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCTokenResponseAction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCTokenResponseAction.java
index da1a579b..d66b8a9e 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCTokenResponseAction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCTokenResponseAction.java
@@ -37,7 +37,7 @@ import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
  * {@link ProfileRequestContext#getOutboundMessageContext()}. Extends base class that offers actions on
  * {@link TokenRequest} found via {@link MessageContext#getMessage()}.
  */
-abstract class AbstractOIDCTokenResponseAction extends AbstractOIDCTokenRequestAction {
+public abstract class AbstractOIDCTokenResponseAction extends AbstractOIDCTokenRequestAction {
 
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(AbstractOIDCTokenResponseAction.class);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAccessTokenHashToIDToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAccessTokenHashToIDToken.java
index 30602d48..fd3a2d39 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAccessTokenHashToIDToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAccessTokenHashToIDToken.java
@@ -52,7 +52,7 @@ public class AddAccessTokenHashToIDToken extends AbstractOIDCSigningResponseActi
             return;
         }
         final AccessTokenHash atHash = AccessTokenHash.compute(getOidcResponseContext().getAccessToken(),
-                new JWSAlgorithm(getSignatureSigningParameters().getSignatureAlgorithm()));
+                new JWSAlgorithm(getSignatureSigningParameters().getSignatureAlgorithm()), null);
         if (atHash == null || atHash.getValue() == null) {
             log.error("{} Not able to generate at_hash using algorithm {}", getLogPrefix(),
                     getSignatureSigningParameters().getSignatureAlgorithm());
@@ -63,7 +63,6 @@ public class AddAccessTokenHashToIDToken extends AbstractOIDCSigningResponseActi
         getOidcResponseContext().getIDToken().setClaim(IDTokenClaimsSet.AT_HASH_CLAIM_NAME, atHash.getValue());
         log.debug("{} Updated token {}", getLogPrefix(),
                 getOidcResponseContext().getIDToken().toJSONObject().toJSONString());
-
     }
 
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSet.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSet.java
index 2c92c468..189eaf4a 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSet.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSet.java
@@ -119,7 +119,6 @@ public class AddAttributesToClaimsSet extends AbstractOIDCResponseAction {
     /** List of claim names that will not be added. */
     @Nullable @NonnullElements private List<String> reservedClaimNames;
 
-
     /** Attributes to include in ID token no matter what. */
     @Nullable @NonnullElements private Set<String> alwaysIncludedAttributes;
 
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContext.java
deleted file mode 100644
index befe031a..00000000
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContext.java
+++ /dev/null
@@ -1,332 +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.impl;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.function.Function;
-import java.util.function.Predicate;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.action.EventIds;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.nimbusds.oauth2.sdk.Scope;
-import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
-import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
-
-import net.minidev.json.JSONArray;
-import net.shibboleth.idp.authn.context.SubjectContext;
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseConsentContext;
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseTokenClaimsContext;
-import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.OIDCAuthenticationResponseContextLookupFunction;
-import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
-import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
-import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
-import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
-import net.shibboleth.idp.profile.IdPEventIds;
-import net.shibboleth.idp.profile.context.navigate.ResponderIdLookupFunction;
-import net.shibboleth.oidc.profile.config.logic.AttributeConsentFlowEnabledPredicate;
-import net.shibboleth.oidc.profile.config.navigate.AccessTokenLifetimeLookupFunction;
-
-import org.opensaml.messaging.context.navigate.ChildContextLookup;
-import org.opensaml.profile.action.ActionSupport;
-
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-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.security.DataSealer;
-import net.shibboleth.utilities.java.support.security.DataSealerException;
-import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrategy;
-import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifierGenerationStrategy;
-
-/**
- * Action that creates a Access Token, and sets it to work context
- * {@link OIDCAuthenticationResponseContext#getAccessToken()} located under
- * {@link ProfileRequestContext#getOutboundMessageContext()}.
- */
-public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction {
-
-    /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(SetAccessTokenToResponseContext.class);
-
-    /** Data sealer for handling access token. */
-    @NonnullAfterInit private DataSealer dataSealer;
-
-    /** Authorize Code / Refresh Token the access token is based on. */
-    @Nullable private TokenClaimsSet tokenClaimsSet;
-
-    /** Strategy used to obtain the response issuer value. */
-    @Nonnull private Function<ProfileRequestContext, String> issuerLookupStrategy;
-
-    /** Strategy used to obtain the access token lifetime. */
-    @Nonnull private Function<ProfileRequestContext,Duration> accessTokenLifetimeLookupStrategy;
-    
-    /** Predicate used to check if consent is enabled with a given {@link ProfileRequestContext}. */
-    @Nonnull
-    private Predicate<ProfileRequestContext> consentEnabledPredicate;
-
-    /** Access Token lifetime. */
-    @Nullable private Duration accessTokenLifetime;
-    
-    /** Subject context. */
-    @Nullable private SubjectContext subjectCtx;
-
-    /** The generator to use. */
-    @Nullable private IdentifierGenerationStrategy idGenerator;
-
-    /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
-    @Nonnull private Function<ProfileRequestContext, IdentifierGenerationStrategy> idGeneratorLookupStrategy;
-
-    /** Authentication request the token is based on. */
-    @Nullable private AuthenticationRequest authenticationRequest;
-
-    /** Strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext}. */
-    @Nonnull
-    private Function<ProfileRequestContext,OIDCAuthenticationResponseTokenClaimsContext>
-    tokenClaimsContextLookupStrategy;
-    
-    /** Strategy used to locate the {@link OIDCAuthenticationResponseConsentContext}. */
-    @Nonnull
-    private Function<ProfileRequestContext, OIDCAuthenticationResponseConsentContext> consentContextLookupStrategy;
-
-    /**
-     * Constructor.
-     */
-    public SetAccessTokenToResponseContext() {
-        tokenClaimsContextLookupStrategy =
-                new ChildContextLookup<>(OIDCAuthenticationResponseTokenClaimsContext.class).compose(
-                        new OIDCAuthenticationResponseContextLookupFunction());
-        consentContextLookupStrategy =
-                new ChildContextLookup<>(OIDCAuthenticationResponseConsentContext.class).compose(
-                        new OIDCAuthenticationResponseContextLookupFunction());
-        accessTokenLifetimeLookupStrategy = new AccessTokenLifetimeLookupFunction();
-        consentEnabledPredicate = new AttributeConsentFlowEnabledPredicate();
-        issuerLookupStrategy = new ResponderIdLookupFunction();
-        idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
-    }
-
-    /**
-     * Set the data sealer instance to use.
-     * 
-     * @param sealer data sealer to use
-     */
-    public void setDataSealer(@Nonnull final DataSealer sealer) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        dataSealer = Constraint.isNotNull(sealer, "DataSealer cannot be null");
-    }
-    
-    /**
-     * Set the strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext} associated with a given
-     * {@link ProfileRequestContext}.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setOIDCAuthenticationResponseTokenClaimsContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, OIDCAuthenticationResponseTokenClaimsContext> strategy) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        tokenClaimsContextLookupStrategy = Constraint.isNotNull(strategy,
-                "OIDCAuthenticationResponseTokenClaimsContextt lookup strategy cannot be null");
-    }
-
-    /**
-     * Set the strategy used to locate the {@link OIDCAuthenticationResponseConsentContext} associated with a given
-     * {@link ProfileRequestContext}.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setOIDCAuthenticationResponseConsentContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, OIDCAuthenticationResponseConsentContext> strategy) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        consentContextLookupStrategy = Constraint.isNotNull(strategy,
-                "OIDCAuthenticationResponseConsentContext lookup strategy cannot be null");
-    }
-
-    /**
-     * Set the strategy used to obtain the access token lifetime.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setAccessTokenLifetimeLookupStrategy(@Nonnull final Function<ProfileRequestContext,Duration> strategy) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        
-        accessTokenLifetimeLookupStrategy =
-                Constraint.isNotNull(strategy, "Access token lifetime lookup strategy cannot be null");
-    }
-
-    /**
-     * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setIdentifierGeneratorLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, IdentifierGenerationStrategy> strategy) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
-        idGeneratorLookupStrategy =
-                Constraint.isNotNull(strategy, "Identifier generation strategy cannot be null");
-    }
-
-    /**
-     * Set the strategy used to locate the issuer value to use.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        
-        issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
-    }
-
-    /**
-     * Set the predicate used to check if consent is enabled with a given {@link ProfileRequestContext}.
-     * 
-     * @param predicate predicate used to check if consent is enabled with a given {@link ProfileRequestContext}.
-     */
-    public void setConsentEnabledPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
-        consentEnabledPredicate =
-                Constraint.isNotNull(predicate, "predicate used to check if consent is enabled cannot be null");
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        
-        if (dataSealer == null) {
-            throw new ComponentInitializationException("DataSealer cannot be null");
-        }
-    }
-    
-    // Checkstyle: CyclomaticComplexity OFF
-    /** {@inheritDoc} */
-    @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        if (!super.doPreExecute(profileRequestContext)) {
-            return false;
-        }
-        
-        accessTokenLifetime = accessTokenLifetimeLookupStrategy.apply(profileRequestContext);
-        if (accessTokenLifetime == null) {
-            log.warn("{} No lifetime supplied for access token", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
-            return false;
-        }
-        
-        tokenClaimsSet = getOidcResponseContext().getAuthorizationGrantClaimsSet();
-        if (tokenClaimsSet != null && !(tokenClaimsSet instanceof RefreshTokenClaimsSet)
-                && !(tokenClaimsSet instanceof AuthorizeCodeClaimsSet)) {
-            log.error("{} No token grant if of illegal type", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        } else if (tokenClaimsSet == null) {
-            /*
-             * Alternate path possible only when access token is to be provided by authz endpoint without authorization
-             * code This is the case only with "token id_token" response type. Unusually complex initialization.
-             */
-            subjectCtx = profileRequestContext.getSubcontext(SubjectContext.class, false);
-            if (subjectCtx == null) {
-                log.error("{} No subject context", getLogPrefix());
-                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-                return false;
-            }
-            idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
-            if (idGenerator == null) {
-                log.error("{} No identifier generation strategy", getLogPrefix());
-                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-                return false;
-            }
-            if (profileRequestContext.getInboundMessageContext() == null
-                    || profileRequestContext.getInboundMessageContext().getMessage() == null || !(profileRequestContext
-                            .getInboundMessageContext().getMessage() instanceof AuthenticationRequest)) {
-                log.error("{} No authentication request avalailable", getLogPrefix());
-                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
-                return false;
-            }
-            authenticationRequest =
-                    (AuthenticationRequest) profileRequestContext.getInboundMessageContext().getMessage();
-        }
-        return true;
-    }
-    // Checkstyle: CyclomaticComplexity ON
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        final Instant dateExp = Instant.now().plus(accessTokenLifetime);
-        ClaimsSet claims = null;
-        ClaimsSet claimsUI = null;
-        final OIDCAuthenticationResponseTokenClaimsContext tokenClaimsCtx =
-                tokenClaimsContextLookupStrategy.apply(profileRequestContext);
-        if (tokenClaimsCtx != null) {
-            claims = tokenClaimsCtx.getClaims();
-            claimsUI = tokenClaimsCtx.getUserinfoClaims();
-        }
-        final AccessTokenClaimsSet claimsSet;
-        if (tokenClaimsSet != null) {
-            // We may not use original claims as input for scope / delivery claims as they may have been reduced.
-            claimsSet = new AccessTokenClaimsSet.Builder(tokenClaimsSet,
-                    getOidcResponseContext().getScope() != null ? getOidcResponseContext().getScope() : new Scope(),
-                    claims, claimsUI, Instant.now(), dateExp).build();
-        } else {
-            final OIDCAuthenticationResponseConsentContext consentCtx =
-                    consentContextLookupStrategy.apply(profileRequestContext);
-            final JSONArray consented = consentCtx != null ? consentCtx.getConsentedAttributes() : null;
-            // "token id_token" response type. Access token is not derived from Authorization code / Refresh token..
-            claimsSet = new AccessTokenClaimsSet.Builder()
-                    .setJWTID(idGenerator)
-                    .setClientID(authenticationRequest.getClientID())
-                    .setIssuer(issuerLookupStrategy.apply(profileRequestContext))
-                    .setPrincipal(subjectCtx.getPrincipalName())
-                    .setSubject(getOidcResponseContext().getSubject())
-                    .setIssuedAt(Instant.now())
-                    .setExpiresAt(dateExp)
-                    .setAuthenticationTime(getOidcResponseContext().getAuthTime())
-                    .setRedirectURI(getOidcResponseContext().getRedirectURI())
-                    .setScope(getOidcResponseContext().getScope())
-                    .setACR(getOidcResponseContext().getAcr())
-                    .setNonce(authenticationRequest.getNonce())
-                    .setClaimsRequest(authenticationRequest.getOIDCClaims())
-                    .setDlClaims(claims)
-                    .setDlClaimsUI(claimsUI)
-                    .setConsentedClaims(consented)
-                    .setConsentEnabled(consentEnabledPredicate.test(profileRequestContext))
-                    .build();
-        }
-        
-        try {
-            getOidcResponseContext().setAccessToken(claimsSet.serialize(dataSealer), accessTokenLifetime);
-            log.debug("{} Setting access token {} as {} to response context ", getLogPrefix(), claimsSet.serialize(),
-                    getOidcResponseContext().getAccessToken());
-        } catch (final DataSealerException e) {
-            log.error("{} Access Token generation failed {}", getLogPrefix(), e.getMessage());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
-        }
-
-    }
-
-}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContext.java
index a1033fac..9b784a4a 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContext.java
@@ -289,6 +289,8 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOIDCAuthentic
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
+        final OIDCAuthenticationResponseContext responseCtx = getOidcResponseContext();
+        
         final OIDCAuthenticationResponseConsentContext consentCtx =
                 consentContextLookupStrategy.apply(profileRequestContext);
         final JSONArray consented = consentCtx != null ? consentCtx.getConsentedAttributes() : null;
@@ -303,22 +305,23 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOIDCAuthentic
             claimsUI = tokenClaimsCtx.getUserinfoClaims();
         }
         final Instant dateExp = Instant.now().plus(authzCodeLifetime);
-        final Scope scope = getOidcResponseContext().getScope();
+        final Scope scope = responseCtx.getScope();
         final AuthorizeCodeClaimsSet claimsSet = new AuthorizeCodeClaimsSet.Builder()
                 .setJWTID(idGenerator)
                 .setClientID(getAuthenticationRequest().getClientID())
                 .setIssuer(issuerLookupStrategy.apply(profileRequestContext))
                 .setPrincipal(subjectCtx.getPrincipalName())
-                .setSubject(getOidcResponseContext().getSubject())
+                .setSubject(responseCtx.getSubject())
                 .setIssuedAt(Instant.now())
                 .setExpiresAt(dateExp)
-                .setAuthenticationTime(getOidcResponseContext().getAuthTime())
-                .setRedirectURI(getOidcResponseContext().getRedirectURI())
+                .setAuthenticationTime(responseCtx.getAuthTime())
+                .setRedirectURI(responseCtx.getRedirectURI())
                 .setScope(scope != null ? scope : new Scope())
-                .setACR(getOidcResponseContext().getAcr())
+                .setAudience(responseCtx.getAudience())
+                .setACR(responseCtx.getAcr())
                 .setNonce(new DefaultRequestNonceLookupFunction().apply(profileRequestContext))
                 .setCodeChallenge(codeChallenge)
-                .setClaimsRequest(getOidcResponseContext().getRequestedClaims())
+                .setClaimsRequest(responseCtx.getRequestedClaims())
                 .setDlClaims(claims)
                 .setDlClaimsID(claimsID)
                 .setDlClaimsUI(claimsUI)
@@ -326,11 +329,11 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOIDCAuthentic
                 .setConsentEnabled(consentEnabledPredicate.test(profileRequestContext))
                 .build();
         // We set token claims set to response context for possible access token generation.
-        getOidcResponseContext().setAuthorizationGrantClaimsSet(claimsSet);
+        responseCtx.setAuthorizationGrantClaimsSet(claimsSet);
         try {
-            getOidcResponseContext().setAuthorizationCode(claimsSet.serialize(dataSealer));
+            responseCtx.setAuthorizationCode(claimsSet.serialize(dataSealer));
             log.debug("{} Setting authz code {} as {} to response context ", getLogPrefix(), claimsSet.serialize(),
-                    getOidcResponseContext().getAuthorizationCode());
+                    responseCtx.getAuthorizationCode());
         } catch (final DataSealerException e) {
             log.error("{} Authorization Code generation failed {}", getLogPrefix(), e.getMessage());
             ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetTokenDeliveryAttributesFromTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetTokenDeliveryAttributesFromTokenToResponseContext.java
index 048afbc8..7ff1d0c4 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetTokenDeliveryAttributesFromTokenToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetTokenDeliveryAttributesFromTokenToResponseContext.java
@@ -39,7 +39,7 @@ import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
 /**
  * Action that locates any token delivery claims from authorization code / access token. For located claims
  * {@link OIDCAuthenticationResponseTokenClaimsContext} is created under {@link OIDCAuthenticationResponseContext} and
- * the claims are placed there. Token and user info end points use the context for forming response.
+ * the claims are placed there. Token and user info endpoints use the context for forming response.
  **/
 public class SetTokenDeliveryAttributesFromTokenToResponseContext extends AbstractOIDCResponseAction {
 
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
index 6a8920e4..4596acdd 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
@@ -198,14 +198,14 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
                         log.error("{} Replay detected of authz code {}", getLogPrefix(), authzCodeClaimsSet.getID());
                         if (!revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE,
                                 authzCodeClaimsSet.getID())) {
-                            log.warn("{} Fatal error, unable to set entry to revocation cache", getLogPrefix());
+                            log.warn("{} Fatal error, unable to save replayed code to revocation cache", getLogPrefix());
                         }
                         ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
                         return;
                     }
                     tokenClaimsSet = authzCodeClaimsSet;
                 } catch (final DataSealerException | ParseException e) {
-                    log.warn("{} Obtaining authz code failed {}", getLogPrefix(), e.getMessage());
+                    log.warn("{} Unwrapping authz code failed: {}", getLogPrefix(), e.getMessage());
                     ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
                     return;
                 }
@@ -231,7 +231,7 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
                     }
                     tokenClaimsSet = refreshTokenClaimsSet;
                 } catch (final ParseException | DataSealerException e) {
-                    log.warn("{} Obtaining refresh token failed {}", getLogPrefix(), e.getMessage());
+                    log.warn("{} Unwrapping refresh token failed {}", getLogPrefix(), e.getMessage());
                     ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
                     return;
                 }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRedirectURI.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRedirectURI.java
index 7e7029ff..36d9c1fc 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRedirectURI.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRedirectURI.java
@@ -35,7 +35,7 @@ import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
- * Action that validates redirect uri is a expected one. Validated redirect uri is stored to response context.
+ * Action that validates redirect uri is expected. Validated redirect uri is stored to response context.
  */
 public class ValidateRedirectURI extends AbstractOIDCAuthenticationResponseAction {
 
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 ae30bf7a..1b801ea1 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
@@ -49,7 +49,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  * <p>Requested scopes come from the inbound message and are possible but optional for both
  * authorization and token requests. They come from lookup functions aware of each message type.</p>
  * 
- * <p>Previously granted scopes are stored in the response context's slow for previous authorization
+ * <p>Previously granted scopes are stored in the response context's slot for previous authorization
  * grant claims. In the case where no scopes are explicitly requested, we still filter the previous
  * grants against the metadata.</p>
  * 
@@ -155,7 +155,7 @@ public class ValidateScope extends AbstractOIDCAuthenticationResponseAction {
             return;
         }
         
-        // These come from a previous authorization grant (authz code or refresh token).
+        // These come from a previous authorization grant (authz code or access/refresh token).
         Scope previouslyGrantedScopes = null;
         if (getOidcResponseContext().getAuthorizationGrantClaimsSet() != null) {
             previouslyGrantedScopes = getOidcResponseContext().getAuthorizationGrantClaimsSet().getScope();
@@ -188,7 +188,7 @@ public class ValidateScope extends AbstractOIDCAuthenticationResponseAction {
         }
         
         if (requestedScopes.contains(OIDCScopeValue.OFFLINE_ACCESS)) {
-            // DefaultRequestResponseTypeLookupFunction returns response type only in authentication end point.
+            // DefaultRequestResponseTypeLookupFunction returns response type only on authorization end point.
             // It is enough to remove offline_scope in this first validation turn.
             final ResponseType responseType =
                     new DefaultRequestResponseTypeLookupFunction().apply(profileRequestContext);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AttributeConsentEnabledInTokenClaimsSetPredicate.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AttributeConsentEnabledInTokenClaimsSetPredicate.java
index 9b8172b6..a58cc9cf 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AttributeConsentEnabledInTokenClaimsSetPredicate.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AttributeConsentEnabledInTokenClaimsSetPredicate.java
@@ -39,7 +39,7 @@ public class AttributeConsentEnabledInTokenClaimsSetPredicate extends AbstractRe
         final MessageContext outboundMessageCtx = input.getOutboundMessageContext();
         if (outboundMessageCtx != null) {
             final OIDCAuthenticationResponseContext oidcResponseContext = 
-                    outboundMessageCtx.getSubcontext(OIDCAuthenticationResponseContext.class, false);
+                    outboundMessageCtx.getSubcontext(OIDCAuthenticationResponseContext.class);
             if (oidcResponseContext != null && oidcResponseContext.getAuthorizationGrantClaimsSet() != null) {
                 return oidcResponseContext.getAuthorizationGrantClaimsSet().isConsentEnabled();
             }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BuildUserInfoErrorResponseFromEvent.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/BuildUserInfoErrorResponseFromEvent.java
similarity index 91%
rename from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BuildUserInfoErrorResponseFromEvent.java
rename to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/BuildUserInfoErrorResponseFromEvent.java
index ac492442..f1e50494 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BuildUserInfoErrorResponseFromEvent.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/BuildUserInfoErrorResponseFromEvent.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.userinfo.profile.impl;
 
 import org.opensaml.profile.context.EventContext;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -23,6 +23,8 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import com.nimbusds.oauth2.sdk.ErrorObject;
 import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
 
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractBuildErrorResponseFromEvent;
+
 /**
  * This action reads an event from the configured {@link EventContext} lookup strategy, constructs an OIDC user info
  * error response message and attaches it as the outbound message.
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundUserInfoResponseMessage.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/FormOutboundUserInfoResponseMessage.java
similarity index 94%
rename from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundUserInfoResponseMessage.java
rename to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/FormOutboundUserInfoResponseMessage.java
index 541ea765..ceef2a1a 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundUserInfoResponseMessage.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/FormOutboundUserInfoResponseMessage.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.userinfo.profile.impl;
 
 import javax.annotation.Nonnull;
 
@@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
 import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCTokenResponseAction;
 
 /**
  * Action that forms outbound message based on response context. Formed message is set to
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/InitializeOutboundUserInfoResponseMessageContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/InitializeOutboundUserInfoResponseMessageContext.java
similarity index 88%
rename from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/InitializeOutboundUserInfoResponseMessageContext.java
rename to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/InitializeOutboundUserInfoResponseMessageContext.java
index e625c01d..2af7a926 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/InitializeOutboundUserInfoResponseMessageContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/InitializeOutboundUserInfoResponseMessageContext.java
@@ -15,11 +15,13 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+package net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl;
 
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.profile.context.ProfileRequestContext;
 
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractInitializeOutboundResponseMessageContext;
+
 /**
  * Action that adds an outbound {@link MessageContext} and related OIDC contexts to the {@link ProfileRequestContext}
  * not knowing the relying party yet.
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessToken.java
new file mode 100644
index 00000000..54989a7f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessToken.java
@@ -0,0 +1,203 @@
+/*
+ * 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.userinfo.profile.impl;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+
+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.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.CredentialResolver;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.security.criteria.UsageCriterion;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.token.AccessToken; 
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.oidc.security.impl.JWTSignatureValidationUtil;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+import net.shibboleth.utilities.java.support.security.DataSealer;
+import net.shibboleth.utilities.java.support.security.DataSealerException;
+
+/**
+ * Action that parses an access token and initially populates the claims for later
+ * validation.
+ * 
+ * <p>Signed JWTs are also signature-checked here.</p>
+ * 
+ * <p>The parsed token is stored to the response context retrievable as claims via
+ * {@link OIDCAuthenticationResponseContext#getTokenClaimsSet()}. Claims validation takes
+ * place later in order to allow for metadata and relying-party/profile config
+ * lookup to allow for pluggable validation, an overridden OP/issuer name, etc.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link OidcEventIds#INVALID_GRANT}
+ * 
+ * @since 3.2.0
+ */
+public class ParseAccessToken extends AbstractOIDCUserInfoValidationResponseAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ParseAccessToken.class);
+
+    /** Data sealer for unwrapping authorization code. */
+    @NonnullAfterInit private DataSealer dataSealer;
+    
+    /** Source of signing keys. */
+    @Nullable private CredentialResolver credentialResolver;
+    
+    /** Copy of signed JWT for non-opaque access tokens. */
+    @Nullable private SignedJWT signedJWT;
+    
+    /**
+     * Set the data sealer instance to use.
+     * 
+     * @param sealer sealer to use
+     */
+    public void setDataSealer(@Nonnull final DataSealer sealer) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        dataSealer = Constraint.isNotNull(sealer, "DataSealer cannot be null");
+    }
+    
+    /**
+     * Set the source of signing keys to use for JWT signature verification.
+     * 
+     * @param resolver signing key resolver
+     */
+    public void setCredentialResolver(@Nullable final CredentialResolver resolver) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        credentialResolver = resolver;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (dataSealer == null) {
+            throw new ComponentInitializationException("DataSealer cannot be null");
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final AccessToken token = getUserInfoRequest().getAccessToken();
+        if (token == null) {
+            log.error("{} Token missing from request", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+            return;
+        }
+        
+        final AccessTokenClaimsSet accessTokenClaimsSet = parseAccessToken(token);
+        if (accessTokenClaimsSet == null) {
+            log.warn("{} Unable to parse/decode token for validation", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+            return;
+        }
+        
+        log.debug("{} Access token unwrapped: {}", getLogPrefix(), accessTokenClaimsSet.serialize());
+        
+        if (signedJWT != null) {
+            // Check typ header.
+            final JOSEObjectType typ = signedJWT.getHeader().getType();
+            if (typ == null || !"at+jwt".equals(typ.getType())) {
+                log.warn("{} Missing or invalid token type: {}", getLogPrefix(), typ != null ? typ.getType() : "null");
+                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+                return;
+            }
+            
+            if (credentialResolver == null) {
+                log.error("{} No CredentialResolver available, can't verify JWT signature", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+                return;
+            }
+            
+            log.debug("{} Checking JWT signature", getLogPrefix());
+            final Collection<Credential> credList = new ArrayList<>();
+            final CriteriaSet criteriaSet = new CriteriaSet(new UsageCriterion(UsageType.SIGNING));
+            try {
+                final Iterable<Credential> creds = credentialResolver.resolve(criteriaSet);
+                if (creds != null) {
+                    creds.forEach(credList::add);
+                }
+            } catch (final ResolverException e) {
+                log.error("{} Failure resolving signing credentials, can't verify JWT signature", getLogPrefix(), e);
+                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+                return;
+            }
+            final String errorEventId = JWTSignatureValidationUtil.validateSignatureEx(credList, signedJWT,
+                    OidcEventIds.INVALID_GRANT);
+            if (errorEventId != null) {
+                log.warn("{} Signature on token ID '{}' invalid", getLogPrefix(), accessTokenClaimsSet.getID());
+                ActionSupport.buildEvent(profileRequestContext, errorEventId);
+                return;
+            }
+        }
+
+        log.debug("{} Access token {} parsed", getLogPrefix(), accessTokenClaimsSet.getID());
+        getOidcResponseContext().setAuthorizationGrantClaimsSet(accessTokenClaimsSet);
+    }
+
+    /**
+     * Attempt to parse token.
+     * 
+     * @param token the token
+     * 
+     * @return parsed claim set or null
+     */
+    @Nullable protected AccessTokenClaimsSet parseAccessToken(@Nonnull @NotEmpty final AccessToken token) {
+        
+        // Try parsing as a JWT.
+        try {
+            signedJWT = SignedJWT.parse(token.getValue());
+            return AccessTokenClaimsSet.parse(signedJWT, dataSealer);
+        } catch (final DataSealerException | ParseException e) {
+            
+        }
+
+        // Fall back to opaque.
+        try {
+            return AccessTokenClaimsSet.parse(token.getValue(), dataSealer);
+        } catch (final DataSealerException | ParseException e) {
+            
+        }
+        
+        return null;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SignUserInfoResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/SignUserInfoResponse.java
similarity index 96%
rename from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SignUserInfoResponse.java
rename to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/SignUserInfoResponse.java
index f974961f..50652c2d 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SignUserInfoResponse.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/SignUserInfoResponse.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.userinfo.profile.impl;
 
 import java.util.function.Function;
 
@@ -36,6 +36,7 @@ import com.nimbusds.openid.connect.sdk.claims.UserInfo;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
 import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultUserInfoSigningAlgLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractSignJWTAction;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessToken.java
index c0e2fe9f..2941b8f1 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessToken.java
@@ -17,109 +17,105 @@
 
 package net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl;
 
-import java.text.ParseException;
+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.opensaml.storage.RevocationCache;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
-import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.oidc.profile.config.navigate.IssuedClaimsValidatorLookupFunction;
 import net.shibboleth.oidc.profile.core.OidcEventIds;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.security.DataSealer;
-import net.shibboleth.utilities.java.support.security.DataSealerException;
 
 /**
- * Action that validates access token is a valid one. Token is valid if it is successfully unwrapped, parsed as access
- * token, is not expired and authorize code it has been derived from has not been revoked. Validated token is stored to
- * response context retrievable as claims {@link OIDCAuthenticationResponseContext#getTokenClaimsSet()}.
+ * Action that validates the claims pulled from an access token as usable for access
+ * to the OP's UserInfo endpoint.
+ * 
+ * <p>The parsed claims are pulled from
+ * {@link OIDCAuthenticationResponseContext#getAuthorizationGrantClaimsSet()}.</p>
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ * @event {@link OidcEventIds#INVALID_GRANT}
  */
 public class ValidateAccessToken extends AbstractOIDCUserInfoValidationResponseAction {
 
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(ValidateAccessToken.class);
-
-    /** Data sealer for unwrapping authorization code. */
-    @NonnullAfterInit private DataSealer dataSealer;
-
-    /** Message revocation cache instance to use. */
-    @NonnullAfterInit private RevocationCache revocationCache;
     
-    /**
-     * Set the data sealer instance to use.
-     * 
-     * @param sealer sealer to use
-     */
-    public void setDataSealer(@Nonnull final DataSealer sealer) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        dataSealer = Constraint.isNotNull(sealer, "DataSealer cannot be null");
-    }
+    /** Lookup strategy for claims validator. */
+    @Nonnull private Function<ProfileRequestContext,ClaimsValidator> claimsValidatorLookupStrategy;
     
+    /** The claims validator to use. */
+    @Nullable private ClaimsValidator claimsValidator;
+
+    /** Constructor. */
+    public ValidateAccessToken() {
+        claimsValidatorLookupStrategy = new IssuedClaimsValidatorLookupFunction();
+    }
+
     /**
-     * Set the revocation cache instance to use.
+     * Set the claims validator lookup strategy.
      * 
-     * @param cache revocation cache to set
+     * @param strategy lookup strategy
      */
-    public void setRevocationCache(@Nonnull final RevocationCache cache) {
+    public void setClaimsValidatorLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,ClaimsValidator> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        revocationCache = Constraint.isNotNull(cache, "RevocationCache cannot be null");
+        claimsValidatorLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
     }
-
+        
     /** {@inheritDoc} */
     @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
         
-        if (revocationCache == null || dataSealer == null) {
-            throw new ComponentInitializationException("RevocationCache and DataSealer cannot be null");
+        claimsValidator = claimsValidatorLookupStrategy.apply(profileRequestContext);
+        if (claimsValidator == null) {
+            log.error("{} Unable to obtain ClaimsValidator to apply", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
         }
+        
+        return true;
     }
     
+    
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-
-        final AccessTokenClaimsSet accessTokenClaimsSet;
-        try {
-            accessTokenClaimsSet =
-                    AccessTokenClaimsSet.parse(getUserInfoRequest().getAccessToken().getValue(), dataSealer);
-            log.debug("{} Access token unwrapped: {}", getLogPrefix(), accessTokenClaimsSet.serialize());
-        } catch (final DataSealerException | ParseException e) {
-            log.warn("{} Parsing access token failed: {}", getLogPrefix(), e.getMessage());
-            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
-            return;
-        }
         
-        if (!accessTokenClaimsSet.isTimeValid()) {
-            log.warn("{} Access token is expired or future dated", getLogPrefix());
+        final TokenClaimsSet tokenClaims = getOidcResponseContext().getAuthorizationGrantClaimsSet();
+        if (!(tokenClaims instanceof AccessTokenClaimsSet) || tokenClaims.getClaimsSet() == null) {
+            log.error("{} Claims validation failed, unable to locate access token claims set to validate", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
             return;
         }
-        
-        if (!accessTokenClaimsSet.getAudience().isEmpty()) {
-            log.warn("{} Access token was not issued for use by this OP");
-            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
-            return;
-        }
-        
-        if (revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, accessTokenClaimsSet.getID())) {
-            log.warn("{} Authorization code {} and all derived tokens have been revoked", getLogPrefix(),
-                    accessTokenClaimsSet.getID());
+
+        log.debug("{} Validating parsed/decoded claims set: {}", getLogPrefix(), tokenClaims.getClaimsSet().toString());
+        try {
+            claimsValidator.validate(tokenClaims.getClaimsSet(), profileRequestContext);
+        } catch (final JWTValidationException e) {
+            log.warn("{} Claims validation failed, token is invalid: {}", getLogPrefix(), e.getMessage());
             ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
             return;
         }
-        
-        log.debug("{} Access token {} validated", getLogPrefix(), accessTokenClaimsSet.getID());
-        getOidcResponseContext().setAuthorizationGrantClaimsSet(accessTokenClaimsSet);
+
+        log.debug("{} Access token {} validated", getLogPrefix(), tokenClaims.getID());
     }
 
 }
\ No newline at end of file
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 394f3440..ce0505dc 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
@@ -157,6 +157,7 @@
                 <value>acr</value>
                 <value>amr</value>
                 <value>auth_time</value>
+                <value>for_op</value>
             </list>
         </property>
     </bean>
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 57d27858..61033f4a 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
@@ -45,9 +45,6 @@
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetRequestedClaimsToResponseContext" scope="prototype"
         p:transcoderRegistry-ref="shibboleth.AttributeRegistryService" />
 
-    <bean id="VerifyRequestedSubjectIdentifier"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.VerifyRequestedSubjectIdentifier" scope="prototype" />
-
     <bean id="PopulatePostAuthnInterceptContext"
             class="net.shibboleth.idp.profile.interceptor.impl.PopulateProfileInterceptorContext" scope="prototype"
             p:availableFlows="#{@'shibboleth.ProfileInterceptorFlowDescriptorManager'.getComponents()}"
@@ -182,34 +179,13 @@
     <bean id="ValidateScope" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateScope" scope="prototype"
         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"
-        c:strategy-ref="shibboleth.MessageContextLookup.Inbound"
-        p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
-        p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver">
-        <property name="activationCondition">
-            <ref bean="IDTokenRequested" />
-        </property>
-        <property name="securityParametersContextLookupStrategy">
-            <bean parent="shibboleth.Functions.Compose"
-                c:g-ref="shibboleth.ChildLookupOrCreate.SecurityParameters"
-                c:f-ref="shibboleth.ChildLookup.RelyingParty" />
-        </property>
-        <property name="existingParametersContextLookupStrategy">
-            <bean parent="shibboleth.Functions.Compose"
-                c:g-ref="shibboleth.ChildLookup.SecurityParameters"
-                c:f-ref="shibboleth.MessageContextLookup.Outbound" />
-        </property>
-    </bean>
+    <bean id="ValidateAudience"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateAudience" scope="prototype"
+        p:requestedAudienceLookupStrategy-ref="AuthenticationRequestAudienceLookupStrategy"
+        p:allowedAudienceLookupStrategy="#{getObject('shibboleth.oidc.AllowedAudienceStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedAudienceStrategy')}" />
 
-    <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">
-        <property name="activationCondition">
-            <ref bean="IDTokenRequested" />
-        </property>
-    </bean>
+    <bean id="AuthenticationRequestAudienceLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultRequestAudienceLookupFunction" />
 
     <bean id="shibboleth.oidc.EncryptionParametersResolver"
         class="net.shibboleth.idp.plugin.oidc.op.security.impl.OIDCClientInformationEncryptionParametersResolver"
@@ -219,6 +195,171 @@
         class="org.opensaml.storage.impl.client.PopulateClientStorageLoadContext" scope="prototype"
         p:storageServices="#{ getObject('shibboleth.ClientStorageServices') ?: getObject('shibboleth.DefaultClientStorageServices') }" />
 
+    <bean id="RevokeConsent" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.RevokeConsent" scope="prototype" />
+
+    <bean id="SetAuthenticationTimeToResponseContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationTimeToResponseContext" scope="prototype" />
+
+    <bean id="SetSectorIdentifierForAttributeResolution"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSectorIdentifierForAttributeResolution" scope="prototype" />
+
+    <bean id="SetAuthenticationContextClassReferenceToResponseContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationContextClassReferenceToResponseContext"
+        scope="prototype" />
+
+    <!--
+    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="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="InitializeAudienceRelyingPartyContextFromSAMLPeer"
+        class="net.shibboleth.idp.saml.profile.impl.InitializeRelyingPartyContextFromSAMLPeer" scope="prototype"
+        p:relyingPartyContextCreationStrategy-ref="AudienceRelyingPartyCreationStrategy"
+        p:peerEntityContextLookupStrategy-ref="LookupOutboundPeerEntityContext" />
+
+    <bean id="LookupOutboundPeerEntityContext" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookup.SAMLPeerEntityContext"
+        c:f-ref="shibboleth.MessageContextLookup.Outbound"/>
+
+    <bean id="SelectAudienceRelyingPartyConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy"
+        p:relyingPartyConfigurationResolver-ref="shibboleth.CriteriaRelyingPartyConfigurationResolver" />
+
+    <bean id="SelectAudienceProfileConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy"
+        p:profileId="#{T(net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenAudienceConfiguration).PROFILE_ID}" />
+
+    <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" />
+
+    <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:proxiedRequesterContextLookupStrategy-ref="AudienceProxiedRequesterLookupFunction"
+        p:proxiedRequesterMetadataContextLookupStrategy-ref="LookupOutboundSAMLEntityContext" />
+
+    <bean id="AudienceIssuerLookupFunction"
+        class="net.shibboleth.idp.profile.context.navigate.ResponderIdLookupFunction"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />
+
+    <bean id="AudienceProxiedRequesterLookupFunction" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+                c:type="#{ T(org.opensaml.profile.context.ProxiedRequesterContext) }" />
+        </constructor-arg>
+        <constructor-arg name="f">
+            <ref bean="shibboleth.MessageContextLookup.Outbound" />
+        </constructor-arg>
+    </bean>
+
+    <bean id="LookupOutboundSAMLEntityContext" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookup.SAMLMetadataContext"
+        c:f-ref="LookupOutboundPeerEntityContext"/>
+
+    <!-- Back to token prep. -->
+
+    <bean id="SetSubjectToResponseContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSubjectToResponseContext" scope="prototype">
+        <property name="subjectLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.AttributeResolutionSubjectLookupFunction"
+                p:transcoderRegistry-ref="shibboleth.AttributeRegistryService" />
+        </property>
+        <property name="subjectTypeLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultSubjectTypeStrategy" />
+        </property>
+    </bean>
+
+    <bean id="VerifyRequestedSubjectIdentifier"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.VerifyRequestedSubjectIdentifier" scope="prototype" />
+
     <bean id="SetTokenDeliveryAttributesToResponseContext"
             class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetTokenDeliveryAttributesToResponseContext" scope="prototype"
             p:transcoderRegistry-ref="shibboleth.AttributeRegistryService">
@@ -237,8 +378,6 @@
     <bean id="SetConsentToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetConsentToResponseContext" scope="prototype" />
 
-    <bean id="RevokeConsent" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.RevokeConsent" scope="prototype" />
-
     <bean id="SetAuthorizationCodeToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthorizationCodeToResponseContext" scope="prototype"
         p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}">
@@ -251,106 +390,147 @@
         </property>
     </bean>
 
-    <bean id="SetAccessTokenToResponseContext"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAccessTokenToResponseContext" scope="prototype"
-        p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}">
-        <property name="activationCondition">
-            <ref bean="AccessTokenRequested" />
+    <!-- If access token is strictly for UserInfo endpoint... -->
+
+    <bean id="PopulateUserInfoAccessTokenSignatureSigningParameters"
+            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters"
+            scope="prototype"
+            c:strategy-ref="shibboleth.MessageContextLookup.Inbound"
+            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"
+                c:f-ref="shibboleth.ChildLookup.RelyingParty" />
         </property>
     </bean>
 
-    <bean id="SetSubjectToResponseContext"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSubjectToResponseContext" scope="prototype">
-        <property name="subjectLookupStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.AttributeResolutionSubjectLookupFunction"
-                p:transcoderRegistry-ref="shibboleth.AttributeRegistryService" />
-        </property>
-        <property name="subjectTypeLookupStrategy">
-            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultSubjectTypeStrategy" />
+    <bean id="BuildOIDCAccessToken"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.BuildAccessToken" scope="prototype"
+        p:dataSealer="#{getObject('%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim())}"
+        p:clientIDLookupStrategy-ref="RequestClientIDLookup" />
+        
+    <bean id="RequestClientIDLookup" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ClientIDLookupStrategy"
+        c:f-ref="shibboleth.MessageContextLookup.Inbound" />
+
+    <bean id="SignOIDCAccessToken"
+            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>
+ 
+    <bean id="SetOAuthAccessTokenToResponseContext"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SetAccessTokenToResponseContext"
+        scope="prototype" />
 
-    <bean id="SetAuthenticationTimeToResponseContext"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationTimeToResponseContext" scope="prototype" />
-
-    <bean id="SetSectorIdentifierForAttributeResolution"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSectorIdentifierForAttributeResolution" scope="prototype" />
+    <!-- If access token is also for third-party resource... -->
 
-    <bean id="SetAuthenticationContextClassReferenceToResponseContext"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationContextClassReferenceToResponseContext"
-        scope="prototype" />
+    <bean id="PopulateThirdPartyAccessTokenSignatureSigningParameters"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters"
+        scope="prototype"
+        c:strategy-ref="shibboleth.MessageContextLookup.Outbound"
+        p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
+        p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver"
+        p:securityParametersContextLookupStrategy-ref="AudienceSecurityParametersCreationStrategy" />
+        
+    <bean id="AudienceSecurityParametersCreationStrategy" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookupOrCreate.SecurityParameters"
+        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="AccessTokenClaimsSetLookupFunction"
+        p:reservedClaimNames="#{getObject('shibboleth.oidc.AccessTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultAccessTokenReservedClaimNames')}" />
+
+    <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:issuerLookupStrategy-ref="AudienceIssuerLookupFunction"
+        p:clientIDLookupStrategy-ref="RequestClientIDLookup"
+        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:securityParametersLookupStrategy-ref="AudienceSecurityParametersCreationStrategy"
+        p:typeHeader="at+jwt" />
+
+    <!--  ID token actions. -->
 
-    <bean id="AddIDTokenShell" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddIDTokenShell" scope="prototype">
-        <property name="activationCondition">
-            <ref bean="IDTokenRequested" />
+    <bean id="PopulateIDTokenSignatureSigningParameters"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters " scope="prototype"
+        c:strategy-ref="shibboleth.MessageContextLookup.Inbound"
+        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"
+                c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+        </property>
+        <property name="existingParametersContextLookupStrategy">
+            <bean parent="shibboleth.Functions.Compose"
+                c:g-ref="shibboleth.ChildLookup.SecurityParameters"
+                c:f-ref="shibboleth.MessageContextLookup.Outbound" />
         </property>
     </bean>
 
+    <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" />
+
+    <bean id="AddIDTokenShell" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddIDTokenShell" scope="prototype" />
+
     <bean id="AddAttributeClaimsToIDToken"
             class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAttributesToClaimsSet" scope="prototype"
             p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
-            p:reservedClaimNames="#{getObject('shibboleth.oidc.IDTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultIDTokenReservedClaimNames')}">
-        <property name="activationCondition">
-            <ref bean="IDTokenRequested" />
-        </property>
-    </bean>
+            p:reservedClaimNames="#{getObject('shibboleth.oidc.IDTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultIDTokenReservedClaimNames')}" />
 
     <bean id="AddAuthTimeToIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAuthTimeToIDToken"
-        scope="prototype">
-        <property name="activationCondition">
-            <ref bean="IDTokenRequested" />
-        </property>
-    </bean>
+        scope="prototype" />
 
-    <bean id="AddAcrToIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAcrToIDToken" scope="prototype">
-        <property name="activationCondition">
-            <ref bean="IDTokenRequested" />
-        </property>
-    </bean>
+    <bean id="AddAcrToIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAcrToIDToken"
+        scope="prototype" />
 
     <bean id="AddNonceToIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddNonceToIDToken"
-        scope="prototype">
-        <property name="activationCondition">
-            <ref bean="IDTokenRequested" />
-        </property>
-    </bean>
+        scope="prototype" />
 
     <bean id="AddAccessTokenHashToIDToken"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAccessTokenHashToIDToken" scope="prototype">
+            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAccessTokenHashToIDToken" scope="prototype"
+            p:activationCondition-ref="AccessTokenRequested">
         <property name="securityParametersLookupStrategy">
             <bean parent="shibboleth.Functions.Compose"
                 c:g-ref="shibboleth.ChildLookup.SecurityParameters"
                 c:f-ref="shibboleth.ChildLookup.RelyingParty" />
         </property>
-        <property name="activationCondition">
-            <bean parent="shibboleth.Conditions.AND">
-                <constructor-arg>
-                    <ref bean="AccessTokenRequested" />
-                </constructor-arg>
-                <constructor-arg>
-                    <ref bean="IDTokenRequested" />
-                </constructor-arg>
-            </bean>
-        </property>
     </bean>
 
     <bean id="AddAuthorizationCodeHashToIDToken"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAuthorizationCodeHashToIDToken" scope="prototype">
+            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAuthorizationCodeHashToIDToken" scope="prototype"
+            p:activationCondition-ref="AuthorizeCodeRequested">
         <property name="securityParametersLookupStrategy">
             <bean parent="shibboleth.Functions.Compose"
                 c:g-ref="shibboleth.ChildLookup.SecurityParameters"
                 c:f-ref="shibboleth.ChildLookup.RelyingParty" />
         </property>
-        <property name="activationCondition">
-            <bean parent="shibboleth.Conditions.AND">
-                <constructor-arg>
-                    <ref bean="AuthorizeCodeRequested" />
-                </constructor-arg>
-                <constructor-arg>
-                    <ref bean="IDTokenRequested" />
-                </constructor-arg>
-            </bean>
-        </property>
     </bean>
 
     <bean id="SignIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SignIDToken" scope="prototype">
@@ -359,17 +539,10 @@
                 c:g-ref="shibboleth.ChildLookup.SecurityParameters"
                 c:f-ref="shibboleth.ChildLookup.RelyingParty" />
         </property>
-        <property name="activationCondition">
-            <ref bean="IDTokenRequested" />
-        </property>
     </bean>
 
     <bean id="EncryptIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.EncryptProcessedToken"
-        scope="prototype">
-        <property name="activationCondition">
-            <ref bean="IDTokenRequested" />
-        </property>
-    </bean>
+        scope="prototype" />
 
     <bean id="UpdateSessionWithSPSession"
             class="net.shibboleth.idp.session.impl.UpdateSessionWithSPSession" scope="prototype"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
index 45b92e20..48096df1 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
@@ -50,10 +50,9 @@
         <evaluate expression="ValidateResponseType" />
         <evaluate expression="ValidateCodeChallenge" />
         <evaluate expression="ValidateScope" />
+        <evaluate expression="ValidateAudience" />
         <evaluate expression="SetRequestedClaimsToResponseContext" />
         <evaluate expression="SetRequestedSubjectToResponseContext" />
-        <evaluate expression="PopulateIDTokenSignatureSigningParameters" />
-        <evaluate expression="PopulateIDTokenEncryptionParameters" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="PopulateClientStorageLoadContext" />
     </action-state>
@@ -86,17 +85,81 @@
     <action-state id="SetAuthenticationInformationToResponseContext">
         <evaluate expression="SetAuthenticationContextClassReferenceToResponseContext" />
         <evaluate expression="SetAuthenticationTimeToResponseContext" />
+        <evaluate expression="SetSectorIdentifierForAttributeResolution" />
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="AttributeResolution" />
+        <transition on="proceed" to="CheckForAudience" />
     </action-state>
 
-    <action-state id="AttributeResolution">
-        <evaluate expression="SetSectorIdentifierForAttributeResolution" />
+    <!-- Audience may or may not be a factor. -->
+    <decision-state id="CheckForAudience">
+        <if test="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getAudience().isEmpty()"
+            then="AttributeResolutionForClient"
+            else="LookupAudienceMetadata" />
+    </decision-state>
+
+    <!-- May need to add a second Relying Party for 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.oidc.metadata.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="AttributeResolutionForAudience" />
+    </action-state>
+
+    <action-state id="AttributeResolutionForClient">
         <evaluate expression="ResolveAttributes" />
         <evaluate expression="FilterAttributes" />
         <evaluate expression="RevokeConsent" />
         <evaluate expression="PopulatePostAuthnInterceptContext" />
         <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CheckPostAuthnInterceptContext" />
+    </action-state>
+
+    <action-state id="AttributeResolutionForAudience">
+        <evaluate expression="ResolveAttributesForAudience" />
+        <evaluate expression="FilterAttributesForAudience" />
+        <evaluate expression="RevokeConsent" />
+        <evaluate expression="PopulatePostAuthnInterceptContext" />
+        <evaluate expression="'proceed'" />
+        
         <transition on="proceed" to="CheckPostAuthnInterceptContext" />
     </action-state>
 
@@ -116,19 +179,75 @@
         <evaluate expression="SetSubjectToResponseContext" />
         <evaluate expression="VerifyRequestedSubjectIdentifier" />
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="BuildTokens" />
+        
+        <transition on="proceed" to="BuildCode" />
     </action-state>
 
-    <action-state id="BuildTokens">
+    <action-state id="BuildCode">
         <evaluate expression="SetTokenDeliveryAttributesToResponseContext" />
         <evaluate expression="SetConsentToResponseContext" />
         <evaluate expression="SetAuthorizationCodeToResponseContext" />
-        <evaluate expression="SetAccessTokenToResponseContext" />
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="BuildResponse" />
+        
+        <transition on="proceed" to="CheckIfAccessTokenNeeded" />
+    </action-state>
+
+    <decision-state id="CheckIfAccessTokenNeeded">
+        <if test="AccessTokenRequested.test(opensamlProfileRequestContext)"
+            then="CheckTokenRequirements"
+            else="CheckIfIDTokenNeeded" />
+    </decision-state>
+
+    <decision-state id="CheckTokenRequirements">
+        <if test="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getAudience().isEmpty()"
+            then="BuildTokensForUserInfoAccess"
+            else="BuildTokensForThirdPartyAccess" />
+    </decision-state>
+
+    <!--
+    Note no JWT encryption here. The token shouldn't even be a JWT, but even if it is,
+    the only audience is us, and the client can't be expected to decrypt it, so it
+    wouldn't be usable. If it were encrypted to our key then there would be no point to
+    allowing it to be a JWT.
+    
+    Note also no attribute claims are added to the access token since that isn't a proper
+    delivery mechanism for claims to the OIDC client.
+    -->
+    <action-state id="BuildTokensForUserInfoAccess">
+        <evaluate expression="PopulateUserInfoAccessTokenSignatureSigningParameters" />
+        <evaluate expression="BuildOIDCAccessToken" />
+        <evaluate expression="SignOIDCAccessToken" />
+        <evaluate expression="SetOAuthAccessTokenToResponseContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CheckIfIDTokenNeeded" />
+    </action-state>
+
+    <!--
+    This includes OIDC use cases and all grant types but the primary audience
+    for the access token is the resource/audience. Encryption remains impossible until
+    this supports pure OAuth scenarios.
+    -->
+    <action-state id="BuildTokensForThirdPartyAccess">
+        <evaluate expression="PopulateThirdPartyAccessTokenSignatureSigningParameters" />
+        <evaluate expression="AddAttributeClaimsToAccessToken" />
+        <evaluate expression="BuildAccessToken" />
+        <evaluate expression="SignAccessToken" />
+        <evaluate expression="SetOAuthAccessTokenToResponseContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CheckIfIDTokenNeeded" />
     </action-state>
 
-    <action-state id="BuildResponse">
+    <decision-state id="CheckIfIDTokenNeeded">
+        <if test="IDTokenRequested.test(opensamlProfileRequestContext)"
+            then="BuildIDToken"
+            else="PopulateClientStorageSaveContext" />
+    </decision-state>
+
+    <action-state id="BuildIDToken">
+        <evaluate expression="PopulateIDTokenSignatureSigningParameters" />
+        <evaluate expression="PopulateIDTokenEncryptionParameters" />
         <evaluate expression="AddIDTokenShell" />
         <evaluate expression="AddAttributeClaimsToIDToken" />
         <evaluate expression="AddAuthTimeToIDToken" />
@@ -138,12 +257,12 @@
         <evaluate expression="AddAuthorizationCodeHashToIDToken" />
         <evaluate expression="SignIDToken" />
         <evaluate expression="EncryptIDToken" />
-        <evaluate expression="UpdateSessionWithSPSession" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="PopulateClientStorageSaveContext" />
      </action-state>
 
     <action-state id="PopulateClientStorageSaveContext">
+        <evaluate expression="UpdateSessionWithSPSession" />
         <evaluate expression="PopulateClientStorageSaveContext" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="ClientStorageSave" />
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 2ce9c598..4d4f0962 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
@@ -162,16 +162,12 @@
         p:requestedScopeLookupStrategy-ref="TokenRequestScopeLookupStrategy"
         p:allowedScopeLookupStrategy="#{getObject('shibboleth.oidc.AllowedScopeStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedScopeStrategy')}" />
 
-    <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" />
+    <bean id="TokenRequestScopeLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestScopeLookupFunction" />
 
     <bean id="IssueIDTokenCondition"
         class="net.shibboleth.idp.plugin.oidc.op.profile.logic.IssueIDTokenCondition" />
@@ -315,80 +311,106 @@
 
     <!-- Back to token prep. -->
     
-    <!-- Traditional third-party grant response handling. -->
+    <!-- OIDC response handling for access/refresh tokens. -->
 
-    <bean id="PopulateIDTokenSignatureSigningParameters"
+    <bean id="PopulateUserInfoAccessTokenSignatureSigningParameters"
             class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters"
             scope="prototype"
             c:strategy-ref="shibboleth.MessageContextLookup.Inbound"
             p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
-            p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver"
-            p:activationCondition-ref="IssueIDTokenCondition">
+            p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver">
         <property name="securityParametersContextLookupStrategy">
             <bean parent="shibboleth.Functions.Compose"
                 c:g-ref="shibboleth.ChildLookupOrCreate.SecurityParameters"
                 c:f-ref="shibboleth.ChildLookup.RelyingParty" />
         </property>
-        <property name="existingParametersContextLookupStrategy">
+    </bean>
+
+    <bean id="BuildOIDCAccessToken"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.BuildAccessToken" scope="prototype"
+        p:dataSealer="#{getObject('%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim())}" />
+
+    <bean id="SignOIDCAccessToken"
+            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.MessageContextLookup.Outbound" />
+                c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+        </property>
+    </bean>
+ 
+    <bean id="SetOAuthAccessTokenToResponseContext"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SetAccessTokenToResponseContext"
+        scope="prototype" />
+
+    <bean id="SetRefreshTokenToResponseContext"
+            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetRefreshTokenToResponseContext" scope="prototype"
+            c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}">
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.AND">
+                <constructor-arg>
+                    <list>
+                        <ref bean="IssueIDTokenCondition" />
+                        <bean class="net.shibboleth.oidc.profile.config.logic.RefreshTokensEnabledPredicate" />
+                    </list>
+                </constructor-arg>
+            </bean>
+        </property>
+    </bean>
+
+    <!-- ID token actions. -->
+
+    <bean id="PopulateIDTokenSignatureSigningParameters"
+            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters"
+            scope="prototype"
+            c:strategy-ref="shibboleth.MessageContextLookup.Inbound"
+            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"
+                c:f-ref="shibboleth.ChildLookup.RelyingParty" />
         </property>
     </bean>
 
     <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"
-        p:activationCondition-ref="IssueIDTokenCondition" />
+        p:encryptionParametersResolver-ref="shibboleth.oidc.EncryptionParametersResolver" />
 
     <bean id="shibboleth.oidc.EncryptionParametersResolver"
         class="net.shibboleth.idp.plugin.oidc.op.security.impl.OIDCClientInformationEncryptionParametersResolver"
         p:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache"
         p:keyFetchInterval="%{idp.oidc.jwksuri.fetchInterval:PT30M}" />
 
-    <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()}" />
-
-    <bean id="SetRefreshTokenToResponseContext"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetRefreshTokenToResponseContext" scope="prototype"
-        c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}">
-        <property name="activationCondition">
-            <bean class="net.shibboleth.oidc.profile.config.logic.RefreshTokensEnabledPredicate" />
-        </property>
-    </bean>
-
-    <bean id="AddIDTokenShell" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddIDTokenShell"
-        scope="prototype" p:activationCondition-ref="IssueIDTokenCondition" />
+    <bean id="AddIDTokenShell"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddIDTokenShell" scope="prototype" />
 
     <bean id="AddAttributeClaimsToIDToken"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAttributesToClaimsSet" scope="prototype"
         p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
-        p:reservedClaimNames="#{getObject('shibboleth.oidc.IDTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultIDTokenReservedClaimNames')}"
-        p:activationCondition-ref="IssueIDTokenCondition" />
+        p:reservedClaimNames="#{getObject('shibboleth.oidc.IDTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultIDTokenReservedClaimNames')}" />
 
     <bean id="AddTokenDeliveryAttributesToIDToken"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddTokenDeliveryAttributesToClaimsSet" scope="prototype"
-        p:targetIDToken="true" p:activationCondition-ref="IssueIDTokenCondition" />
+        p:targetIDToken="true" />
 
-    <bean id="AddAuthTimeToIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAuthTimeToIDToken"
-        scope="prototype" p:activationCondition-ref="IssueIDTokenCondition" />
+    <bean id="AddAuthTimeToIDToken"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAuthTimeToIDToken" scope="prototype" />
 
-    <bean id="AddAcrToIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAcrToIDToken"
-        scope="prototype" p:activationCondition-ref="IssueIDTokenCondition" />
+    <bean id="AddAcrToIDToken"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAcrToIDToken" scope="prototype" />
 
-    <bean id="AddNonceToIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddNonceToIDToken"
-        scope="prototype" p:requestNonceLookupStrategy-ref="shibboleth.TokenRequestNonceLookupStrategy"
-        p:activationCondition-ref="IssueIDTokenCondition" />
+    <bean id="AddNonceToIDToken"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddNonceToIDToken" scope="prototype"
+        p:requestNonceLookupStrategy-ref="shibboleth.TokenRequestNonceLookupStrategy" />
 
     <bean id="shibboleth.TokenRequestNonceLookupStrategy"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestNonceLookupFunction"
-        scope="prototype" />
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestNonceLookupFunction" />
 
     <bean id="AddAccessTokenHashToIDToken"
-            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAccessTokenHashToIDToken" scope="prototype"
-            p:activationCondition-ref="IssueIDTokenCondition">
+            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAccessTokenHashToIDToken" scope="prototype">
         <property name="securityParametersLookupStrategy">
             <bean parent="shibboleth.Functions.Compose"
                 c:g-ref="shibboleth.ChildLookup.SecurityParameters"
@@ -396,8 +418,7 @@
         </property>
     </bean>
 
-    <bean id="SignIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SignIDToken" scope="prototype"
-        p:activationCondition-ref="IssueIDTokenCondition">
+    <bean id="SignIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SignIDToken" scope="prototype">
         <property name="securityParametersLookupStrategy">
             <bean parent="shibboleth.Functions.Compose"
                 c:g-ref="shibboleth.ChildLookup.SecurityParameters"
@@ -406,7 +427,7 @@
     </bean>
 
     <bean id="EncryptIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.EncryptProcessedToken"
-        p:activationCondition-ref="IssueIDTokenCondition" scope="prototype" />
+        scope="prototype" />
 
     <bean id="FormOutboundMessage"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.FormOutboundTokenResponseMessage" scope="prototype" />
@@ -420,33 +441,31 @@
         </property>
     </bean>
 
-    <!-- client_credentials grant response actions. -->
+    <!-- Third-party token actions. -->
 
-    <bean id="PopulateAccessTokenSignatureSigningParameters"
-            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters"
-            scope="prototype"
-            c:strategy-ref="shibboleth.MessageContextLookup.Outbound"
-            p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
-            p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver"
-            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="PopulateThirdPartyAccessTokenSignatureSigningParameters"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters"
+        scope="prototype"
+        c:strategy-ref="shibboleth.MessageContextLookup.Outbound"
+        p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
+        p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver"
+        p:securityParametersContextLookupStrategy-ref="AudienceSecurityParametersCreationStrategy" />
         
     <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="PopulateThirdPartyAccessTokenEncryptionParameters"
+            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">
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.NOT" c:_0-ref="IssueIDTokenCondition" />
+        </property>
+    </bean>
             
     <bean id="AudienceEncryptionOptionalPredicate"
         class="net.shibboleth.oidc.profile.config.logic.EncryptionOptionalPredicate"
@@ -487,13 +506,13 @@
         p:typeHeader="at+jwt" />
 
     <bean id="EncryptAccessToken"
-        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.EncryptAccessToken" scope="prototype"
-        p:encryptionContextLookupStrategy-ref="AudienceEncryptionContextCreationStrategy" />
+            class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.EncryptAccessToken" scope="prototype"
+            p:encryptionContextLookupStrategy-ref="AudienceEncryptionContextCreationStrategy">
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.NOT" c:_0-ref="IssueIDTokenCondition" />
+        </property>
+    </bean>
     
-    <bean id="SetOAuthAccessTokenToResponseContext"
-        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SetAccessTokenToResponseContext"
-        scope="prototype" />
-
     <bean id="PostResponsePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
         p:fieldExtractors="#{getObject('shibboleth.oidc.TokenPostResponseAuditExtractors') ?: getObject('shibboleth.oidc.DefaultTokenPostResponseAuditExtractors')}" />
 
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 0f6afca5..a2be67eb 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
@@ -60,11 +60,19 @@
         <evaluate expression="InitializeSubjectContext" />
         <evaluate expression="SetSubjectFromAuthzCodeToResponseContext" />
         <evaluate expression="ValidateScope" />
+        <evaluate expression="ValidateAudience" />
         <evaluate expression="'proceed'" />
         
-        <transition on="proceed" to="CheckAttributeResolutionForClient" />
+        <transition on="proceed" to="CheckTraditionalGrantForAudience" />
     </action-state>
 
+    <!-- For standard grants, audience may or may not be a factor. -->
+    <decision-state id="CheckTraditionalGrantForAudience">
+        <if test="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getAudience().isEmpty()"
+            then="CheckAttributeResolutionForClient"
+            else="LookupAudienceMetadata" />
+    </decision-state>
+
     <!-- These steps apply to grants that are self-contained on this endpoint. -->
     <action-state id="ClientCredentialsGrantProcessing">
         <evaluate expression="SetAuthenticationContextClassReferenceToResponseContext" />
@@ -77,7 +85,7 @@
         <transition on="proceed" to="LookupAudienceMetadata" />
     </action-state>
 
-    <!-- For client credentials grant, need to flip the Relying Party here to the primary resource/audience. -->
+    <!-- May need to add a second Relying Party for the primary resource/audience. -->
 
     <action-state id="LookupAudienceMetadata">
         <evaluate expression="AudienceOIDCMetadataLookup" />
@@ -137,35 +145,10 @@
         <transition on="proceed" to="DoConsentLookup" />
     </action-state>
     
-    <decision-state id="BuildResponse">
-        <if test="NotClientCredentialsGrantCondition.test(opensamlProfileRequestContext)"
-            then="TraditionalGrantResponse"
-            else="ClientCredentialsGrantResponse" />
-    </decision-state>
-    
-    <action-state id="TraditionalGrantResponse">
-        <evaluate expression="PopulateIDTokenSignatureSigningParameters" />
-        <evaluate expression="PopulateIDTokenEncryptionParameters" />
-        <evaluate expression="SetOIDCAccessTokenToResponseContext" />
-        <evaluate expression="SetRefreshTokenToResponseContext" />
-        <evaluate expression="AddIDTokenShell" />
-        <evaluate expression="AddAttributeClaimsToIDToken" />
-        <evaluate expression="AddTokenDeliveryAttributesToIDToken" />
-        <evaluate expression="AddAuthTimeToIDToken" />
-        <evaluate expression="AddAcrToIDToken" />
-        <evaluate expression="AddNonceToIDToken" />
-        <evaluate expression="AddAccessTokenHashToIDToken" />
-        <evaluate expression="SignIDToken" />
-        <evaluate expression="EncryptIDToken" />
-        <evaluate expression="'proceed'" />
-        
-        <transition on="proceed" to="PopulateOutboundInterceptContext" />
-    </action-state>
-
     <decision-state id="CheckAttributeResolutionForAudience">
         <if test="ResolveAttributesForAudiencePredicate.test(opensamlProfileRequestContext)"
             then="AttributeResolutionForAudience"
-            else="ClientCredentialsGrantResponse" />
+            else="DoConsentLookup" />
     </decision-state>
 
     <action-state id="AttributeResolutionForAudience">
@@ -173,17 +156,77 @@
         <evaluate expression="FilterAttributesForAudience" />
         <evaluate expression="'proceed'" />
         
-        <transition on="proceed" to="ClientCredentialsGrantResponse" />
+        <transition on="proceed" to="DoConsentLookup" />
+    </action-state>
+    
+    <decision-state id="BuildResponse">
+        <if test="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getAudience().isEmpty()"
+            then="BuildTokensForUserInfoAccess"
+            else="BuildTokensForThirdPartyAccess" />
+    </decision-state>
+    
+    <!--
+    Note no JWT encryption here. The token shouldn't even be a JWT, but even if it is,
+    the only audience is us, and the client can't be expected to decrypt it, so it
+    wouldn't be usable. If it were encrypted to our key then there would be no point to
+    allowing it to be a JWT.
+    
+    Note also no attribute claims are added to the access token since that isn't a proper
+    delivery mechanism for claims to the OIDC client.
+    -->
+    <action-state id="BuildTokensForUserInfoAccess">
+        <evaluate expression="PopulateUserInfoAccessTokenSignatureSigningParameters" />
+        <evaluate expression="BuildOIDCAccessToken" />
+        <evaluate expression="SignOIDCAccessToken" />
+        <evaluate expression="SetOAuthAccessTokenToResponseContext" />
+        <evaluate expression="SetRefreshTokenToResponseContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="ProducingIDToken" />
     </action-state>
 
-    <action-state id="ClientCredentialsGrantResponse">
-        <evaluate expression="PopulateAccessTokenSignatureSigningParameters" />
-        <evaluate expression="PopulateAccessTokenEncryptionParameters" />
+    <!--
+    
+    This includes OIDC and OAuth use cases and all grant types but the primary audience
+    for the access token is the resource/audience. The OP MAY also be an audience for the
+    token if the "oidc" scope is in play, in which case the actions have to behave differently
+    or be disabled in some cases. Encryption is for example impossible in that case, as with
+    the OIDC-only use of JWTs above.
+    -->
+    <action-state id="BuildTokensForThirdPartyAccess">
+        <evaluate expression="PopulateThirdPartyAccessTokenSignatureSigningParameters" />
+        <evaluate expression="PopulateThirdPartyAccessTokenEncryptionParameters" />
         <evaluate expression="AddAttributeClaimsToAccessToken" />
         <evaluate expression="BuildAccessToken" />
         <evaluate expression="SignAccessToken" />
         <evaluate expression="EncryptAccessToken" />
         <evaluate expression="SetOAuthAccessTokenToResponseContext" />
+        <evaluate expression="SetRefreshTokenToResponseContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="ProducingIDToken" />
+    </action-state>
+
+    <!-- ID token only issued when scope includes "openid" -->
+
+    <decision-state id="ProducingIDToken">
+        <if test="IssueIDTokenCondition.test(opensamlProfileRequestContext)"
+            then="BuildIDToken"
+            else="PopulateOutboundInterceptContext" />
+    </decision-state>
+
+    <action-state id="BuildIDToken">
+        <evaluate expression="PopulateIDTokenSignatureSigningParameters" />
+        <evaluate expression="PopulateIDTokenEncryptionParameters" />
+        <evaluate expression="AddIDTokenShell" />
+        <evaluate expression="AddAttributeClaimsToIDToken" />
+        <evaluate expression="AddTokenDeliveryAttributesToIDToken" />
+        <evaluate expression="AddAuthTimeToIDToken" />
+        <evaluate expression="AddAcrToIDToken" />
+        <evaluate expression="AddNonceToIDToken" />
+        <evaluate expression="AddAccessTokenHashToIDToken" />
+        <evaluate expression="SignIDToken" />
+        <evaluate expression="EncryptIDToken" />
         <evaluate expression="'proceed'" />
         
         <transition on="proceed" to="PopulateOutboundInterceptContext" />
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 86ecda28..f7c36d64 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
@@ -23,20 +23,24 @@
     </bean>
 
     <bean id="InitializeOutboundMessageContext"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeOutboundUserInfoResponseMessageContext"
+        class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.InitializeOutboundUserInfoResponseMessageContext"
         scope="prototype" />
 
-    <bean id="ValidateAccessToken"
-        class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.ValidateAccessToken" scope="prototype"
-        p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
-        p:revocationCache-ref="shibboleth.oidc.RevocationCache" />
-
+    <!-- Used for metadata lookup. -->
     <bean id="shibboleth.ClientIDLookupStrategy"
         class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.UserInfoRequestClientIDLookupFunction" />
 
-    <bean id="shibboleth.UserInfoRequestClientIDLookupStrategy"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.UserInfoRequestClientIDLookupFunction" />
+    <bean id="ParseAccessToken"
+        class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.ParseAccessToken" scope="prototype"
+        p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
+        p:credentialResolver-ref="SigningCredentialsResolver" />
+
+    <bean id="SigningCredentialsResolver" class="net.shibboleth.idp.relyingparty.impl.SigningCredentialsResolver" 
+        c:_0-ref="shibboleth.RelyingPartyResolverService" />
 
+    <bean id="ValidateAccessToken"
+        class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.ValidateAccessToken" scope="prototype" />
+        
     <bean id="ValidateScope" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateScope" scope="prototype"
             p:allowedScopeLookupStrategy="#{getObject('shibboleth.oidc.AllowedScopeStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedScopeStrategy')}">
         <property name="requestedScopeLookupStrategy">
@@ -117,8 +121,8 @@
     <bean id="shibboleth.UserInfoResponseClaimsSetLookupStrategy"
         class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.UserInfoResponseClaimsSetLookupFunction" />
 
-    <bean id="SignUserInfoResponse" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SignUserInfoResponse"
-        scope="prototype">
+    <bean id="SignUserInfoResponse" class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.SignUserInfoResponse"
+            scope="prototype">
         <property name="securityParametersLookupStrategy">
             <bean parent="shibboleth.Functions.Compose"
                 c:g-ref="shibboleth.ChildLookup.SecurityParameters"
@@ -130,10 +134,10 @@
         scope="prototype" />
 
     <bean id="FormOutboundMessage"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.FormOutboundUserInfoResponseMessage" scope="prototype" />
+        class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.FormOutboundUserInfoResponseMessage" scope="prototype" />
 
     <bean id="BuildErrorResponseFromEvent"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.BuildUserInfoErrorResponseFromEvent" scope="prototype"
+        class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.BuildUserInfoErrorResponseFromEvent" scope="prototype"
         p:httpServletResponse-ref="shibboleth.HttpServletResponse"
         p:mappedErrors-ref="shibboleth.oidc.ErrorMappings">
         <property name="eventContextLookupStrategy">
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-flow.xml
index e22cf7bb..be5f41c5 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-flow.xml
@@ -15,8 +15,8 @@
 
     <action-state id="DecodeMessage">
         <evaluate expression="DecodeMessage" />
-        <evaluate expression="ValidateAccessToken" />
         <evaluate expression="PostDecodePopulateAuditContext" />
+        <evaluate expression="ParseAccessToken" />
         <evaluate expression="'proceed'" />
         
         <!-- DoMetadataLookup is expected to proceed to SelectConfiguration -->
@@ -44,6 +44,7 @@
     </subflow-state>
 
     <action-state id="OutboundContextsAndSecurityParameters">
+        <evaluate expression="ValidateAccessToken" />
         <evaluate expression="ValidateScope" />
         <evaluate expression="SetRequestedClaimsToResponseContext" />
         <evaluate expression="SetTokenDeliveryAttributesFromTokenToResponseContext" />
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 e79e5955..455e4139 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
@@ -24,6 +24,7 @@
         p:forcePKCE="%{idp.oidc.forcePKCE:false}"
         p:allowPKCEPlain="%{idp.oidc.allowPKCEPlain:false}"
         p:iDTokenLifetime="%{idp.oidc.idToken.defaultLifetime:PT1H}"
+        p:accessTokenType="%{idp.oauth2.accessToken.type:}"
         p:accessTokenLifetime="%{idp.oidc.accessToken.defaultLifetime:PT10M}"
         p:refreshTokenLifetime="%{idp.oidc.refreshToken.defaultLifetime:PT2H}"
         p:alwaysIncludedAttributes="%{idp.oidc.alwaysIncludedAttributes:}" />
@@ -40,7 +41,8 @@
         class="net.shibboleth.oidc.profile.config.OIDCUserInfoConfiguration"
         p:issuer-ref="shibboleth.oidc.issuer"
         p:encryptionOptional="%{idp.oidc.encryptionOptional:true}"
-        p:deniedUserInfoAttributes="%{idp.oidc.deniedUserInfoAttributes:}" />
+        p:deniedUserInfoAttributes="%{idp.oidc.deniedUserInfoAttributes:}"
+        p:issuedClaimsValidator-ref="DefaultUserInfoJWTClaimsValidator" />
         
     <bean id="OIDC.Registration" parent="AbstractOIDCProfile" lazy-init="true"
         class="net.shibboleth.oidc.profile.config.OIDCDynamicRegistrationConfiguration"
@@ -214,6 +216,10 @@
             <bean parent="shibboleth.MDDrivenDurationProperty" p:propertyName="iDTokenLifetime"
                 p:defaultValue="%{idp.oidc.idToken.defaultLifetime:PT1H}" />
         </property>
+        <property name="accessTokenTypeLookupStrategy">
+            <bean parent="shibboleth.MDDrivenStringProperty" p:propertyName="accessTokenType"
+                p:defaultValue="%{idp.oidc.accessToken.type:}" />
+        </property>
         <property name="accessTokenLifetimeLookupStrategy">
             <bean parent="shibboleth.MDDrivenDurationProperty" p:propertyName="accessTokenLifetime"
                 p:defaultValue="%{idp.oidc.accessToken.defaultLifetime:PT10M}" />
@@ -309,6 +315,11 @@
                 </property>
             </bean>
         </property>
+        <property name="issuedClaimsValidatorLookupStrategy">
+            <bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="issuedClaimsValidator"
+                p:propertyType="#{T(net.shibboleth.oidc.jwt.claims.ClaimsValidator)}"
+                p:defaultValue-ref="DefaultUserInfoJWTClaimsValidator" />
+        </property>
     </bean>
         
     <bean id="OIDC.Registration.MDDriven" parent="AbstractMDDrivenOIDCFlowAwareProfile" lazy-init="true"
@@ -500,7 +511,7 @@
         <ref bean="JWTIdentifierClaimsValidator" />
     </util:list>
 
-    <!-- Default issued JWT validation wiring (for introspection/revocation). -->
+    <!-- Default issued JWT validation wiring (for introspection/revocation/UserInfo). -->
 
     <bean id="DefaultIntrospectionJWTClaimsValidator"
         class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
@@ -510,6 +521,10 @@
         class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
         p:claimValidators-ref="RevocationClaimsValidators" />
 
+    <bean id="DefaultUserInfoJWTClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+        p:claimValidators-ref="UserInfoClaimsValidators" />
+
     <bean id="SelfIssuedClaimsValidator"
             class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
             p:claimName="iss">
@@ -575,6 +590,33 @@
         </bean>
     </util:list>
 
+    <bean id="OPInAudienceClaimsValidator"
+            class="net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator"
+            p:allowMissing="true">
+        <property name="audienceLookupStrategy">
+            <bean class="net.shibboleth.utilities.java.support.logic.BiFunctionSupport"
+                factory-method="forFunctionOfFirstArg"
+                    c:_0-ref="shibboleth.ResponderIdLookup.Simple" />
+        </property>
+    </bean>
+
+    <util:list id="UserInfoClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+        <ref bean="RequiredClaimsValidator" />
+        <ref bean="ExpiryClaimsValidator" />
+        <ref bean="NotBeforeClaimsValidator" />
+        <ref bean="SelfIssuedClaimsValidator" />
+        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator" p:requireAll="false">
+            <property name="claimValidators">
+                <list value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+                    <ref bean="ClientIDClaimsValidator" />
+                    <ref bean="LegacyClientIDClaimsValidator" />
+                </list>
+            </property>
+        </bean>
+        <ref bean="OPInAudienceClaimsValidator" />
+        <ref bean="JWTIDRevocationClaimsValidator" />
+    </util:list>
+
     <!--
     Auto-wiring exposers for credentials to get them loaded into the IdP's relying party config resolver.
     The qualifiers control which auto-wiring point is used.
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 d883801b..c4acd64c 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,6 +21,7 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
 
+import net.shibboleth.idp.authn.context.SubjectContext;
 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;
@@ -57,12 +58,14 @@ import com.nimbusds.oauth2.sdk.id.ClientID;
 public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
 
     /** Action to test. */
-    private BuildAccessToken action;
+    @Nullable private BuildAccessToken action;
     
     @BeforeMethod
     protected void setUp() throws Exception {
         super.setUp();
         
+        profileRequestCtx.getSubcontext(SubjectContext.class, true).setPrincipalName("jdoe");
+        
         respCtx.setAuthTime(Instant.now());
         respCtx.setSubject(clientId);
         respCtx.setAcr("0");
@@ -142,9 +145,8 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
                     throws ComponentInitializationException, NoSuchAlgorithmException {
         if ("JWT".equals(type)) {
             action.setAccessTokenTypeLookupStrategy(FunctionSupport.constant("JWT"));
-        } else if (type == null) {
-            action.setDataSealer(getDataSealer());
         }
+        action.setDataSealer(getDataSealer());
         action.setClientIDLookupStrategy(FunctionSupport.constant(new ClientID(clientId)));
         action.initialize();
     }
@@ -179,6 +181,7 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
             assertTrue(at.getIssuedAt().isBefore(Instant.now()));
             assertEquals(at.getScope(), scope);
             assertEquals(at.getSubject(), clientId);
+            assertEquals(at.getPrincipal(), "jdoe");
         } else if (ctx.getJWT() != null) {
             final JWTClaimsSet claims = ctx.getJWT().getJWTClaimsSet();
             assertNotNull(claims);
@@ -191,6 +194,10 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
             assertTrue(claims.getIssueTime().toInstant().isBefore(Instant.now()));
             assertEquals(claims.getStringClaim(TokenClaimsSet.KEY_SCOPE), scope.toString());
             assertEquals(claims.getSubject(), clientId);
+            
+            final JWTClaimsSet unsealedClaims =
+                    JWTClaimsSet.parse(getDataSealer().unwrap(claims.getStringClaim(TokenClaimsSet.KEY_SEALED_FOR_OP)));
+            assertEquals(unsealedClaims.getStringClaim(TokenClaimsSet.KEY_USER_PRINCIPAL), "jdoe");
         } else {
             throw new RuntimeException("No token found");
         }
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 b5a6b294..be7781d1 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
@@ -243,7 +243,7 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
         metadata.setIDTokenJWEEnc(encMethod);
         metadata.setTokenEndpointAuthMethod(tokenEndpointMethod);
         metadata.setUserInfoJWSAlg(userInfoSigAlg);
-        metadata.setCustomField("audience", List.of("https://rp.example.org", "https://rp2.example.org"));
+        metadata.setCustomField("audience", List.of("https://rp.example.org", "https://rp2.example.org", "https://resource.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/AuthorizeFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
index 62bb2070..13fb2f9d 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
@@ -18,9 +18,12 @@
 package net.shibboleth.idp.plugin.oidc.op.profile.flow;
 
 import java.io.IOException;
+import java.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.Collections;
 import java.util.Date;
+import java.util.List;
 
 import org.opensaml.storage.StorageService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -36,13 +39,13 @@ import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.PlainJWT;
 import com.nimbusds.jwt.SignedJWT;
-import com.nimbusds.oauth2.sdk.ParseException;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
 import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
 import com.nimbusds.openid.connect.sdk.claims.ClaimRequirement;
 import com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest;
 
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
 import net.shibboleth.idp.session.SessionException;
 import net.shibboleth.oidc.profile.core.OidcError;
@@ -55,6 +58,8 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     
     public static final String FLOW_ID = "oidc/authorize";
     
+    String resource = "https://resource.example.org";
+    String issuer = "https://op.example.org";
     String redirectUri = "https://example.org/cb";
     String clientId = "mockClientId";
     String clientSecret = "mockClientSecretmockClientSecretmockClientSecretmockClientSecretmockClientSecret";
@@ -74,7 +79,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithAuthorizationCodeFlow() throws IOException, ParseException, SessionException {
+    public void testWithAuthorizationCodeFlow() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=code&scope=openid%20profile&redirect_uri="
                 + redirectUri);
@@ -92,7 +97,25 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithAuthorizationCodeFlowNoOpenid() throws IOException, ParseException, SessionException {
+    public void testWithAuthorizationCodeFlowAndResource() throws IOException, SessionException {
+        request.setMethod("GET");
+        request.setQueryString("client_id=mockClientId&response_type=code&scope=openid%20profile&resource=" + resource +
+                "&redirect_uri=" + redirectUri);
+        storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNull(successResponse.getIDToken());
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+    }
+    
+    @Test
+    public void testWithAuthorizationCodeFlowNoOpenid() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=code&scope=profile&redirect_uri="
                 + redirectUri);
@@ -105,7 +128,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithImplicitFlowNoNonce() throws IOException, ParseException, SessionException {
+    public void testWithImplicitFlowNoNonce() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=id_token&scope=openid%20profile&redirect_uri="
                 + redirectUri);
@@ -118,7 +141,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithImplicitFlow() throws IOException, ParseException, SessionException {
+    public void testWithImplicitFlow() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=id_token&scope=openid%20profile&redirect_uri="
                 + redirectUri + "&nonce=idhas3h23hi13h1o2i32");
@@ -136,7 +159,25 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithImplicitTokenFlow() throws IOException, ParseException, SessionException {
+    public void testWithImplicitFlowAndResource() throws IOException, SessionException {
+        request.setMethod("GET");
+        request.setQueryString("client_id=mockClientId&response_type=id_token&scope=openid%20profile&resource=" + resource
+                + "&redirect_uri=" + redirectUri + "&nonce=idhas3h23hi13h1o2i32");
+        storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNotNull(successResponse.getIDToken());
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNull(successResponse.getAuthorizationCode());
+    }
+
+    @Test
+    public void testWithImplicitTokenFlow() throws IOException, SessionException, DataSealerException, ParseException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=id_token+token&scope=openid%20profile"
                 + "&redirect_uri="+ redirectUri + "&nonce=idhas3h23hi13h1o2i32");
@@ -151,10 +192,37 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNotNull(successResponse.getIDToken());
         Assert.assertNotNull(successResponse.getAccessToken());
         Assert.assertNull(successResponse.getAuthorizationCode());
+        
+        final AccessTokenClaimsSet token =
+                AccessTokenClaimsSet.parse(successResponse.getAccessToken().getValue(), getDataSealer());
+        Assert.assertEquals(token.getAudience(), Collections.singletonList(issuer));
+        Assert.assertNull(token.getClaimsSet().getStringClaim("eduPersonScopedAffiliation"));
     }
 
     @Test
-    public void testWithImplicitTokenFlowNoNonce() throws IOException, ParseException, SessionException {
+    public void testWithImplicitTokenFlowAndResource() throws IOException, SessionException, ParseException {
+        request.setMethod("GET");
+        request.setQueryString("client_id=mockClientId&response_type=id_token+token&scope=openid%20profile"
+                + "&resource=" + resource + "&redirect_uri="+ redirectUri + "&nonce=idhas3h23hi13h1o2i32");
+        storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNotNull(successResponse.getIDToken());
+        Assert.assertNotNull(successResponse.getAccessToken());
+        Assert.assertNull(successResponse.getAuthorizationCode());
+
+        final JWTClaimsSet token =  SignedJWT.parse(successResponse.getAccessToken().getValue()).getJWTClaimsSet();
+        Assert.assertEquals(token.getAudience(), List.of(resource, issuer));
+        Assert.assertNotNull(token.getStringClaim("eduPersonScopedAffiliation"));
+    }
+
+    @Test
+    public void testWithImplicitTokenFlowNoNonce() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=id_token+token&scope=openid%20profile"
                 + "&redirect_uri="+ redirectUri);
@@ -167,7 +235,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithImplicitFlowNoOpenIdScope() throws IOException, ParseException, SessionException {
+    public void testWithImplicitFlowNoOpenIdScope() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=id_token&scope=profile&redirect_uri="
                 + redirectUri + "&nonce=idhas3h23hi13h1o2i32");
@@ -180,7 +248,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithHybridIdTokenFlow() throws IOException, ParseException, SessionException {
+    public void testWithHybridIdTokenFlow() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=code+id_token&scope=openid%20profile"
                 + "&redirect_uri=" + redirectUri + "&nonce=idhas3h23hi13h1o2i32");
@@ -198,7 +266,25 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithHybridIdTokenFlowNoNonce() throws IOException, ParseException, SessionException {
+    public void testWithHybridIdTokenFlowAndResource() throws IOException, SessionException {
+        request.setMethod("GET");
+        request.setQueryString("client_id=mockClientId&response_type=code+id_token&scope=openid%20profile"
+                + "&resource=" + resource + "&redirect_uri=" + redirectUri + "&nonce=idhas3h23hi13h1o2i32");
+        storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNotNull(successResponse.getIDToken());
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+    }
+
+    @Test
+    public void testWithHybridIdTokenFlowNoNonce() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=code+id_token&scope=openid%20profile"
                 + "&redirect_uri=" + redirectUri);
@@ -211,7 +297,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithHybridTokenFlow() throws IOException, ParseException, SessionException {
+    public void testWithHybridTokenFlow() throws IOException, SessionException, ParseException, DataSealerException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=code+token&scope=openid%20profile"
                 + "&redirect_uri="+ redirectUri);
@@ -227,10 +313,38 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNull(successResponse.getIDToken());
         Assert.assertNotNull(successResponse.getAccessToken());
         Assert.assertNotNull(successResponse.getAuthorizationCode());
+        
+        final AccessTokenClaimsSet token =
+                AccessTokenClaimsSet.parse(successResponse.getAccessToken().getValue(), getDataSealer());
+        Assert.assertEquals(token.getAudience(), Collections.singletonList(issuer));
+        Assert.assertNull(token.getClaimsSet().getStringClaim("eduPersonScopedAffiliation"));
     }
 
     @Test
-    public void testWithHybridIdTokenTokenFlow() throws IOException, ParseException, SessionException {
+    public void testWithHybridTokenFlowAndResource() throws IOException, SessionException, ParseException {
+        request.setMethod("GET");
+        request.setQueryString("client_id=mockClientId&response_type=code+token&scope=openid%20profile"
+                + "&resource=" + resource + "&redirect_uri="+ redirectUri);
+        storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        // success response as id_token is not involved and thus nonce is not required
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNull(successResponse.getIDToken());
+        Assert.assertNotNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+
+        final JWTClaimsSet token =  SignedJWT.parse(successResponse.getAccessToken().getValue()).getJWTClaimsSet();
+        Assert.assertEquals(token.getAudience(), List.of(resource, issuer));
+        Assert.assertNotNull(token.getStringClaim("eduPersonScopedAffiliation"));
+    }
+
+    @Test
+    public void testWithHybridIdTokenTokenFlow() throws IOException, SessionException, ParseException, DataSealerException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=code+id_token+token&scope=openid%20profile"
                 + "&redirect_uri="+ redirectUri + "&nonce=idhas3h23hi13h1o2i32");
@@ -245,10 +359,37 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNotNull(successResponse.getIDToken());
         Assert.assertNotNull(successResponse.getAccessToken());
         Assert.assertNotNull(successResponse.getAuthorizationCode());
+
+        final AccessTokenClaimsSet token =
+                AccessTokenClaimsSet.parse(successResponse.getAccessToken().getValue(), getDataSealer());
+        Assert.assertEquals(token.getAudience(), Collections.singletonList(issuer));
+        Assert.assertNull(token.getClaimsSet().getStringClaim("eduPersonScopedAffiliation"));
+    }
+
+    @Test
+    public void testWithHybridIdTokenTokenFlowAndResource() throws IOException, SessionException, ParseException {
+        request.setMethod("GET");
+        request.setQueryString("client_id=mockClientId&response_type=code+id_token+token&scope=openid%20profile"
+                + "&resource=" + resource + "&redirect_uri="+ redirectUri + "&nonce=idhas3h23hi13h1o2i32");
+        storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNotNull(successResponse.getIDToken());
+        Assert.assertNotNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+
+        final JWTClaimsSet token =  SignedJWT.parse(successResponse.getAccessToken().getValue()).getJWTClaimsSet();
+        Assert.assertEquals(token.getAudience(), List.of(resource, issuer));
+        Assert.assertNotNull(token.getStringClaim("eduPersonScopedAffiliation"));
     }
 
     @Test
-    public void testWithHybridIdTokenTokenFlowNoNonce() throws IOException, ParseException, SessionException {
+    public void testWithHybridIdTokenTokenFlowNoNonce() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=code+id_token+token&scope=openid%20profile"
                 + "&redirect_uri="+ redirectUri);
@@ -261,7 +402,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithAuthorizationCodeFlowUnforcedPKCE() throws IOException, ParseException, SessionException {
+    public void testWithAuthorizationCodeFlowUnforcedPKCE() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientIdPKCEPlainUnforced&response_type=code&scope=openid%20profile&redirect_uri="
                 + redirectUri);
@@ -279,7 +420,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithAuthorizationCodeFlowForcedPlainPKCEMissingChallenge() throws IOException, ParseException, SessionException {
+    public void testWithAuthorizationCodeFlowForcedPlainPKCEMissingChallenge() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientIdPKCEPlain&response_type=code&scope=openid%20profile&redirect_uri="
                 + redirectUri);
@@ -293,7 +434,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithAuthorizationCodeFlowForcedPlainPKCEUnknownChallenge() throws IOException, ParseException, SessionException {
+    public void testWithAuthorizationCodeFlowForcedPlainPKCEUnknownChallenge() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientIdPKCEPlain&response_type=code&scope=openid%20profile&redirect_uri="
                 + redirectUri + "&code_challenge=osdfojsfod&code_challenge_method=unsupported");
@@ -307,7 +448,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithAuthorizationCodeFlowForcedPlainPKCEValidChallenge() throws IOException, ParseException, SessionException {
+    public void testWithAuthorizationCodeFlowForcedPlainPKCEValidChallenge() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientIdPKCEPlain&response_type=code&scope=openid%20profile&redirect_uri="
                 + redirectUri + "&code_challenge=osdfojsfod&code_challenge_method=plain");
@@ -325,7 +466,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithAuthorizationCodeFlowForcedS256PKCEPlainChallenge() throws IOException, ParseException, SessionException {
+    public void testWithAuthorizationCodeFlowForcedS256PKCEPlainChallenge() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientIdPKCES256&response_type=code&scope=openid%20profile&redirect_uri="
                 + redirectUri + "&code_challenge=osdfojsfod&code_challenge_method=plain");
@@ -339,7 +480,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithAuthorizationCodeFlowForcedS256PKCEUnknownChallenge() throws IOException, ParseException, SessionException {
+    public void testWithAuthorizationCodeFlowForcedS256PKCEUnknownChallenge() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientIdPKCES256&response_type=code&scope=openid%20profile&redirect_uri="
                 + redirectUri + "&code_challenge=osdfojsfod&code_challenge_method=unknown");
@@ -353,7 +494,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithAuthorizationCodeFlowForcedS256PKCEValidChallenge() throws IOException, ParseException, SessionException {
+    public void testWithAuthorizationCodeFlowForcedS256PKCEValidChallenge() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientIdPKCES256&response_type=code&scope=openid%20profile&redirect_uri="
                 + redirectUri + "&code_challenge=osdfojsfod&code_challenge_method=S256");
@@ -371,7 +512,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithAuthorizationCodeFlowNoScopes() throws IOException, ParseException, SessionException {
+    public void testWithAuthorizationCodeFlowNoScopes() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=code&scope=openid%20profile&redirect_uri="
                 + redirectUri);
@@ -387,7 +528,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
     
     @Test
-    public void testWithAuthorizationCodeFlowWithIDTokenClaims() throws IOException, ParseException, SessionException, java.text.ParseException, DataSealerException {
+    public void testWithAuthorizationCodeFlowWithIDTokenClaims() throws IOException, SessionException, DataSealerException, ParseException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=code&scope=openid%20profile"
                 + "&claims=%7B%22id_token%22%3A%7B%22email%22%3A%7B%22essential%22%3Atrue%7D%7D%7D"
@@ -416,7 +557,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
     
     @Test
-    public void testWithAuthorizationCodeFlowWithUIClaims() throws IOException, ParseException, SessionException, java.text.ParseException, DataSealerException {
+    public void testWithAuthorizationCodeFlowWithUIClaims() throws IOException, SessionException, DataSealerException, ParseException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockClientId&response_type=code&scope=openid%20profile"
                 + "&claims=%7B%22userinfo%22%3A%7B%22email%22%3A%7B%22essential%22%3Atrue%7D%7D%7D"
@@ -445,7 +586,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithAuthorizationCodeFlowUsingSAMLMetadata() throws IOException, ParseException, SessionException {
+    public void testWithAuthorizationCodeFlowUsingSAMLMetadata() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=mockSamlClientId&response_type=code&scope=openid%20profile&redirect_uri="
                 + redirectUri);
@@ -463,7 +604,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
     
     @Test
-    public void testWithAuthorizationCodeFlowUsingUntrustedRP() throws IOException, ParseException, SessionException {
+    public void testWithAuthorizationCodeFlowUsingUntrustedRP() throws IOException, SessionException {
         request.setMethod("GET");
         request.setQueryString("client_id=notTrusted&response_type=code&scope=openid%20profile&redirect_uri="
                 + redirectUri);
@@ -475,7 +616,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithPlainReqObjectExpired() throws IOException, ParseException, SessionException {
+    public void testWithPlainReqObjectExpired() throws IOException, SessionException {
         final JWTClaimsSet ro = new JWTClaimsSet.Builder()
                 .expirationTime(Date.from(Instant.now().minus(Duration.ofMinutes(5))))
                 .build();
@@ -483,7 +624,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithPlainReqObjectNbfInFuture() throws IOException, ParseException, SessionException {
+    public void testWithPlainReqObjectNbfInFuture() throws IOException, SessionException {
         final JWTClaimsSet ro = new JWTClaimsSet.Builder()
                 .notBeforeTime(Date.from(Instant.now().plus(Duration.ofMinutes(5))))
                 .build();
@@ -491,7 +632,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithPlainReqObjectOverwriteRedirectUri() throws IOException, ParseException, SessionException {
+    public void testWithPlainReqObjectOverwriteRedirectUri() throws IOException, SessionException {
         final JWTClaimsSet ro = new JWTClaimsSet.Builder()
                 .claim("redirect_uri", redirectUri)
                 .build();
@@ -513,8 +654,8 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithPlainReqObjectClaimsRequest() throws IOException, ParseException, SessionException,
-            java.text.ParseException, DataSealerException {
+    public void testWithPlainReqObjectClaimsRequest() throws IOException, SessionException,
+            DataSealerException, ParseException {
         final String payload = "{\n"
                 + "  \"iss\": \"" + clientId + "\",\n"
                 + "  \"response_type\": \"code\",\n"
@@ -578,16 +719,16 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithSignedReqObjectNoIssuer() throws IOException, ParseException, SessionException,
+    public void testWithSignedReqObjectNoIssuer() throws IOException, SessionException,
             JOSEException {
         final JWTClaimsSet ro = new JWTClaimsSet.Builder()
-                .audience("https://op.example.org")
+                .audience(issuer)
                 .build();
         assertRequestObjectError(createSecretJWT(ro, clientSecret));
     }
 
     @Test
-    public void testWithSignedReqObjectNoAudience() throws IOException, ParseException, SessionException,
+    public void testWithSignedReqObjectNoAudience() throws IOException, SessionException,
             JOSEException {
         final JWTClaimsSet ro = new JWTClaimsSet.Builder()
                 .issuer(clientId)
@@ -596,17 +737,17 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithSignedReqObjectWrongIssuer() throws IOException, ParseException, SessionException,
+    public void testWithSignedReqObjectWrongIssuer() throws IOException, SessionException,
             JOSEException {
         final JWTClaimsSet ro = new JWTClaimsSet.Builder()
-                .audience("https://op.example.org")
+                .audience(issuer)
                 .issuer("invalid")
                 .build();
         assertRequestObjectError(createSecretJWT(ro, clientSecret));
     }
 
     @Test
-    public void testWithSignedReqObjectWrongAudience() throws IOException, ParseException, SessionException,
+    public void testWithSignedReqObjectWrongAudience() throws IOException, SessionException,
             JOSEException {
         final JWTClaimsSet ro = new JWTClaimsSet.Builder()
                 .audience("https://invalid.org")
@@ -619,7 +760,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     public void testWithSignedReqObjectOverwriteRedirectUri() throws IOException, ParseException,
             SessionException, JOSEException {
         final JWTClaimsSet ro = new JWTClaimsSet.Builder()
-                .audience("https://op.example.org")
+                .audience(issuer)
                 .issuer(clientId)
                 .claim("redirect_uri", redirectUri)
                 .build();
@@ -641,8 +782,8 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
     }
 
     @Test
-    public void testWithSignedReqObjectClaimsRequest() throws IOException, ParseException,
-            SessionException, JOSEException, java.text.ParseException, DataSealerException {
+    public void testWithSignedReqObjectClaimsRequest() throws IOException,
+            SessionException, JOSEException, DataSealerException, ParseException {
         final String payload = "{\n"
                 + "  \"iss\": \"" + clientId + "\",\n"
                 + "  \"response_type\": \"code\",\n"
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
index 01fd363e..64cbe96c 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
@@ -129,7 +129,12 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         assertErrorCode(result, OAuth2Error.INVALID_GRANT_CODE);
     }
 
-    @Test
+    /**
+     * TODO: This test "fails" now because it's honoring a non-OIDC request by assuming there has to be
+     * a requested and allowed audience/resource. The original success outcome was an anomaly due to the
+     * original grant handling not supporting the audience notion.
+     */
+    @Test(enabled=false)
     public void testNoScopes() throws Exception {
         setHttpFormRequest("POST",
                 createRequestParameters(redirectUri, "authorization_code",
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 6eb852d5..f2e0ff6a 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
@@ -91,15 +91,15 @@ public abstract class BaseOIDCResponseActionTest extends OpenSAMLInitBaseTestCas
 
     protected ProfileRequestContext profileRequestCtx;
 
-    Credential credentialRSA;
+    protected Credential credentialRSA;
 
-    Credential credentialEC256;
+    protected Credential credentialEC256;
 
-    Credential credentialEC384;
+    protected Credential credentialEC384;
 
-    Credential credentialEC521;
+    protected Credential credentialEC521;
 
-    Credential credentialHMAC;
+    protected Credential credentialHMAC;
 
     public BaseOIDCResponseActionTest() {
         subject = "generatedSubject";
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContextTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContextTest.java
deleted file mode 100644
index e6fa6333..00000000
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContextTest.java
+++ /dev/null
@@ -1,283 +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.impl;
-
-import net.shibboleth.idp.authn.context.SubjectContext;
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseConsentContext;
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseTokenClaimsContext;
-import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
-import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
-import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
-import net.shibboleth.idp.profile.IdPEventIds;
-import net.shibboleth.idp.profile.context.RelyingPartyContext;
-import net.shibboleth.idp.profile.testing.ActionTestingSupport;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-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.Instant;
-
-import org.opensaml.profile.action.EventIds;
-import org.springframework.webflow.execution.Event;
-import org.testng.Assert;
-import org.testng.annotations.Test;
-
-import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
-import com.nimbusds.oauth2.sdk.Scope;
-import com.nimbusds.oauth2.sdk.TokenRequest;
-import com.nimbusds.oauth2.sdk.id.ClientID;
-import com.nimbusds.oauth2.sdk.token.RefreshToken;
-import com.nimbusds.openid.connect.sdk.claims.ACR;
-
-// Checkstyle: ThrowsCount OFF
-
-/** {@link SetAccessTokenToResponseContext} unit test. */
-public class SetAccessTokenToResponseContextTest extends BaseOIDCResponseActionTest {
-
-    /** Action to test. */
-    private SetAccessTokenToResponseContext action;
-
-    private void init() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException {
-        respCtx.setScope(new Scope());
-        final TokenClaimsSet claims = new AuthorizeCodeClaimsSet.Builder()
-                .setJWTID(idGenerator)
-                .setClientID(new ClientID())
-                .setIssuer("issuer")
-                .setPrincipal("userPrin")
-                .setSubject("subject")
-                .setIssuedAt(Instant.now())
-                .setExpiresAt(Instant.now())
-                .setAuthenticationTime(Instant.now())
-                .setRedirectURI(new URI("http://example.com"))
-                .setScope(new Scope())
-                .setACR(new ACR("0"))
-                .build();
-        respCtx.setSubject("subject");
-        respCtx.setAuthTime(Instant.now());
-        respCtx.setAuthorizationGrantClaimsSet(claims);
-        respCtx.setAcr("0");
-        respCtx.setRedirectURI(new URI("http://example.com"));
-        action = new SetAccessTokenToResponseContext();
-        action.setDataSealer(getDataSealer());
-        action.initialize();
-        final SubjectContext subjectCtx = profileRequestCtx.getSubcontext(SubjectContext.class, true);
-        subjectCtx.setPrincipalName("userPrin");
-    }
-
-    /**
-     * Basic success case.
-     * 
-     * @throws ComponentInitializationException 
-     * @throws NoSuchAlgorithmException 
-     * @throws URISyntaxException 
-     * @throws ParseException 
-     * @throws DataSealerException 
-     */
-    @Test
-    public void testSuccess() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
-            ParseException, DataSealerException {
-        init();
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertProceedEvent(event);
-        Assert.assertNotNull(respCtx.getAccessToken());
-        final AccessTokenClaimsSet at =
-                AccessTokenClaimsSet.parse(respCtx.getAccessToken().getValue(), getDataSealer());
-        Assert.assertNotNull(at);
-    }
-
-    /**
-     * Basic success case for non derived token.
-     * 
-     * @throws ComponentInitializationException 
-     * @throws NoSuchAlgorithmException 
-     * @throws URISyntaxException 
-     * @throws ParseException 
-     * @throws DataSealerException 
-     */
-    @Test
-    public void testSuccess2() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
-            ParseException, DataSealerException {
-        init();
-        respCtx.setAuthorizationGrantClaimsSet(null);
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertProceedEvent(event);
-        Assert.assertNotNull(respCtx.getAccessToken());
-        final AccessTokenClaimsSet at =
-                AccessTokenClaimsSet.parse(respCtx.getAccessToken().getValue(), getDataSealer());
-        Assert.assertNotNull(at);
-    }
-
-    /**
-     * Basic success case for non derived token. Test for consent.
-     * 
-     * @throws ComponentInitializationException 
-     * @throws NoSuchAlgorithmException 
-     * @throws URISyntaxException 
-     * @throws ParseException 
-     * @throws DataSealerException 
-     */
-    @Test
-    public void testSuccess2Consent() throws ComponentInitializationException, NoSuchAlgorithmException,
-            URISyntaxException, ParseException, DataSealerException {
-        init();
-        respCtx.setAuthorizationGrantClaimsSet(null);
-        final OIDCAuthenticationResponseConsentContext consCtx =
-                (OIDCAuthenticationResponseConsentContext) respCtx.addSubcontext(
-                        new OIDCAuthenticationResponseConsentContext());
-        consCtx.getConsentedAttributes().add("3");
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertProceedEvent(event);
-        Assert.assertNotNull(respCtx.getAccessToken());
-        final AccessTokenClaimsSet at =
-                AccessTokenClaimsSet.parse(respCtx.getAccessToken().getValue(), getDataSealer());
-        Assert.assertNotNull(at);
-        Assert.assertEquals(at.getConsentedClaims(), consCtx.getConsentedAttributes());
-    }
-
-    /**
-     * Basic success case with delivery claims.
-     * 
-     * @throws ComponentInitializationException 
-     * @throws NoSuchAlgorithmException 
-     * @throws URISyntaxException 
-     * @throws ParseException 
-     * @throws DataSealerException 
-     */
-    @Test
-    public void testSuccessWithTokenDelivery() throws ComponentInitializationException, NoSuchAlgorithmException,
-            URISyntaxException, ParseException, DataSealerException {
-        init();
-        final OIDCAuthenticationResponseTokenClaimsContext tokenCtx =
-                (OIDCAuthenticationResponseTokenClaimsContext) respCtx.addSubcontext(
-                        new OIDCAuthenticationResponseTokenClaimsContext());
-        tokenCtx.getClaims().setClaim("1", "1");
-        tokenCtx.getIdtokenClaims().setClaim("2", "2");
-        tokenCtx.getUserinfoClaims().setClaim("3", "3");
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertProceedEvent(event);
-        Assert.assertNotNull(respCtx.getAccessToken());
-        final AccessTokenClaimsSet at =
-                AccessTokenClaimsSet.parse(respCtx.getAccessToken().getValue(), getDataSealer());
-        Assert.assertNotNull(at);
-        Assert.assertNotNull(at.getDeliveryClaims().getClaim("1"));
-        Assert.assertNotNull(at.getUserinfoDeliveryClaims().getClaim("3"));
-        Assert.assertNull(at.getIDTokenDeliveryClaims());
-    }
-
-    /**
-     * fails as request is of wrong type.
-     * 
-     * @throws NoSuchAlgorithmException 
-     * @throws ComponentInitializationException 
-     * @throws URISyntaxException 
-     */
-    @Test
-    public void testFailNoAuthnReqCase2()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException {
-        init();
-        respCtx.setAuthorizationGrantClaimsSet(null);
-        final TokenRequest req =
-                new TokenRequest(new URI("http://example.com"), new RefreshTokenGrant(new RefreshToken()), null);
-        setTokenRequest(req);
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MSG_CTX);
-    }
-
-    /**
-     * fails as there is no subject ctx.
-     * 
-     * @throws NoSuchAlgorithmException 
-     * @throws ComponentInitializationException 
-     * @throws URISyntaxException 
-     */
-    @Test
-    public void testFailNoSubjectCtxCase2()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException {
-        init();
-        respCtx.setAuthorizationGrantClaimsSet(null);
-        profileRequestCtx.removeSubcontext(SubjectContext.class);
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, EventIds.INVALID_PROFILE_CTX);
-    }
-
-    /**
-     * fails as there is no rp ctx.
-     * 
-     * @throws NoSuchAlgorithmException 
-     * @throws ComponentInitializationException 
-     * @throws URISyntaxException 
-     */
-    @Test
-    public void testFailNoRPCtx()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException {
-        init();
-        profileRequestCtx.removeSubcontext(RelyingPartyContext.class);
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, IdPEventIds.INVALID_PROFILE_CONFIG);
-    }
-
-    /**
-     * fails as there is no profile conf.
-     * 
-     * @throws NoSuchAlgorithmException 
-     * @throws ComponentInitializationException 
-     * @throws URISyntaxException 
-     */
-    @Test
-    public void testFailNoProfileConf()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException {
-        init();
-        final RelyingPartyContext rpCtx = profileRequestCtx.getSubcontext(RelyingPartyContext.class, false);
-        rpCtx.setProfileConfig(null);
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, IdPEventIds.INVALID_PROFILE_CONFIG);
-    }
-
-    /**
-     * fails as the token is of wrong type.
-     * 
-     * @throws NoSuchAlgorithmException 
-     * @throws ComponentInitializationException 
-     * @throws URISyntaxException 
-     */
-    @Test
-    public void testFailTokenNotCodeOrRefresh()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException {
-        init();
-        final TokenClaimsSet claims = new AccessTokenClaimsSet.Builder()
-                .setJWTID(idGenerator)
-                .setClientID(new ClientID())
-                .setIssuer("issuer")
-                .setPrincipal("userPrin")
-                .setSubject("subject")
-                .setIssuedAt(Instant.now())
-                .setExpiresAt(Instant.now())
-                .setAuthenticationTime(Instant.now())
-                .setRedirectURI(new URI("http://example.com"))
-                .setScope(new Scope())
-                .build();
-        
-        respCtx.setAuthorizationGrantClaimsSet(claims);
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, EventIds.INVALID_PROFILE_CTX);
-    }
-
-}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundUserInfoResponseMessageTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/FormOutboundUserInfoResponseMessageTest.java
similarity index 96%
rename from idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundUserInfoResponseMessageTest.java
rename to idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/FormOutboundUserInfoResponseMessageTest.java
index 192ad118..bfe4d6a6 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundUserInfoResponseMessageTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/FormOutboundUserInfoResponseMessageTest.java
@@ -15,10 +15,11 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+package net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl;
 
 import java.net.URISyntaxException;
 
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import org.opensaml.messaging.context.MessageContext;
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessTokenTest.java
similarity index 58%
copy from idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessTokenTest.java
copy to idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessTokenTest.java
index e2d4be55..0f6c64bf 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessTokenTest.java
@@ -23,17 +23,36 @@ import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
 import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.oidc.security.credential.BasicJWKCredential;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
 import net.shibboleth.utilities.java.support.security.DataSealerException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
 import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
 import java.time.Instant;
 import java.util.Collections;
 
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.CredentialResolver;
 import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
@@ -41,32 +60,51 @@ import com.nimbusds.openid.connect.sdk.UserInfoRequest;
 
 // Checkstyle: ThrowsCount OFF
 
-/** {@link ValidateAccessToken} unit test. */
-public class ValidateAccessTokenTest extends BaseOIDCResponseActionTest {
+/** {@link ParseAccessToken} unit test. */
+public class ParseAccessTokenTest extends BaseOIDCResponseActionTest {
 
-    /** Action to test. */
-    private ValidateAccessToken action;
+    /** Private key for JWT signing. */
+    RSAPrivateKey rsaPrivateKey;
 
-    @BeforeMethod
-    private void init() throws ComponentInitializationException, NoSuchAlgorithmException {
-        action = new ValidateAccessToken();
-        action.setDataSealer(getDataSealer());
-        action.setRevocationCache(new MockRevocationCache(false, true));
-        action.initialize();
-    }
+    /** Public key for JWT signing. */
+    RSAPublicKey rsaPublicKey;
+
+    /** Action to test. */
+    private ParseAccessToken action;
 
     /**
-     * Test that action throws error if revocation cache is not set.
+     * Init keys for JWT signing.
      * 
-     * @throws NoSuchAlgorithmException 
-     * @throws ComponentInitializationException 
+     * @throws NoSuchAlgorithmException on error
      */
-    @Test(expectedExceptions = ComponentInitializationException.class)
-    public void testNoRevocationCache() throws NoSuchAlgorithmException, ComponentInitializationException {
-        action = new ValidateAccessToken();
+    @BeforeClass
+    public void initKeys() throws NoSuchAlgorithmException {
+        final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+        keyGen.initialize(2048);
+        final KeyPair keyPair = keyGen.genKeyPair();
+        rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
+        rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
+    }
+
+    @BeforeMethod
+    private void init() throws ComponentInitializationException, NoSuchAlgorithmException {
+        action = new ParseAccessToken();
         action.setDataSealer(getDataSealer());
+        action.setCredentialResolver(new CredentialResolver() {
+
+            public Iterable<Credential> resolve(CriteriaSet criteria) throws ResolverException {
+                return Collections.singletonList(resolveSingle(criteria));
+            }
+
+            public Credential resolveSingle(CriteriaSet criteria) throws ResolverException {
+                final BasicJWKCredential cred = new BasicJWKCredential();
+                cred.setPublicKey(rsaPublicKey);
+                cred.setPrivateKey(rsaPrivateKey);
+                return cred;
+            }
+            
+        });
         action.initialize();
-        action.execute(requestCtx);
     }
 
     /**
@@ -99,99 +137,77 @@ public class ValidateAccessTokenTest extends BaseOIDCResponseActionTest {
         ActionTestingSupport.assertProceedEvent(event);
     }
 
+
     /**
-     * Fails due to access token containing an audience (ours never do).
+     * Basic success case with signed JWT.
      * 
      * @throws NoSuchAlgorithmException 
      * @throws ComponentInitializationException 
      * @throws URISyntaxException 
      * @throws DataSealerException 
+     * @throws JOSEException 
      */
     @Test
-    public void testFailsAudience()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
+    public void testJWT()
+            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException, JOSEException {
+        
+        final String sealedClaims = getDataSealer().wrap(
+                new JWTClaimsSet.Builder().claim(TokenClaimsSet.KEY_USER_PRINCIPAL, "userPrin").build().toString());
+        
         final TokenClaimsSet claims = new AccessTokenClaimsSet.Builder()
                 .setJWTID(idGenerator)
                 .setClientID(new ClientID())
                 .setIssuer("issuer")
-                .setPrincipal("userPrin")
                 .setSubject("subject")
                 .setIssuedAt(Instant.now())
                 .setExpiresAt(Instant.now().plusSeconds(1))
                 .setAuthenticationTime(Instant.now())
                 .setRedirectURI(new URI("http://example.com"))
                 .setScope(new Scope())
-                .setAudience(Collections.singletonList("foo"))
+                .addCustomClaim(TokenClaimsSet.KEY_SEALED_FOR_OP, sealedClaims)
                 .build();
-        final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
-        final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
+        
+        JWSSigner signer = new RSASSASigner(rsaPrivateKey);
+        JWSHeader.Builder headerBuilder = new JWSHeader.Builder(new JWSAlgorithm("RS256")).type(new JOSEObjectType("at+jwt"));
+        SignedJWT jwt = new SignedJWT(headerBuilder.build(), claims.getClaimsSet());
+        jwt.sign(signer);
+        
+        BearerAccessToken token = new BearerAccessToken(jwt.serialize());
+        UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
         setUserInfoRequest(req);
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_GRANT);
-    }
-    
-    /**
-     * Fails due to access token being substituted with authorize code.
-     * 
-     * @throws NoSuchAlgorithmException 
-     * @throws ComponentInitializationException 
-     * @throws URISyntaxException 
-     * @throws DataSealerException 
-     */
-    @Test
-    public void testFailsNotAccessToken()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
-        final TokenClaimsSet claims = new AuthorizeCodeClaimsSet.Builder()
-                .setJWTID(idGenerator)
-                .setClientID(new ClientID())
-                .setIssuer("issuer")
-                .setPrincipal("userPrin")
-                .setSubject("subject")
-                .setIssuedAt(Instant.now())
-                .setExpiresAt(Instant.now().plusSeconds(1))
-                .setAuthenticationTime(Instant.now())
-                .setRedirectURI(new URI("http://example.com"))
-                .setScope(new Scope())
-                .build();
-        final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
-        final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
+        
+        Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+
+        // Wrong token type.
+        headerBuilder = new JWSHeader.Builder(new JWSAlgorithm("RS256"));
+        jwt = new SignedJWT(headerBuilder.build(), claims.getClaimsSet());
+        jwt.sign(signer);
+        token = new BearerAccessToken(jwt.serialize());
+        req = new UserInfoRequest(new URI("http://example.com"), token);
         setUserInfoRequest(req);
-        final Event event = action.execute(requestCtx);
+        
+        event = action.execute(requestCtx);
         ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_GRANT);
-    }
 
-    /**
-     * Fails due token expiration.
-     * 
-     * @throws NoSuchAlgorithmException 
-     * @throws ComponentInitializationException 
-     * @throws URISyntaxException 
-     * @throws DataSealerException 
-     */
-    @Test
-    public void testFailsExpired()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
-        final TokenClaimsSet claims = new AccessTokenClaimsSet.Builder()
-                .setJWTID(idGenerator)
-                .setClientID(new ClientID())
-                .setIssuer("issuer")
-                .setPrincipal("userPrin")
-                .setSubject("subject")
-                .setIssuedAt(Instant.now())
-                .setExpiresAt(Instant.now().minusMillis(1))
-                .setAuthenticationTime(Instant.now())
-                .setRedirectURI(new URI("http://example.com"))
-                .setScope(new Scope())
-                .build();
-        final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
-        final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
+        // Sign with wrong key.
+        final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+        keyGen.initialize(2048);
+        final KeyPair keyPair = keyGen.genKeyPair();
+        signer = new RSASSASigner(keyPair.getPrivate());
+        headerBuilder = new JWSHeader.Builder(new JWSAlgorithm("RS256")).type(new JOSEObjectType("at+jwt"));
+        jwt = new SignedJWT(headerBuilder.build(), claims.getClaimsSet());
+        jwt.sign(signer);
+        token = new BearerAccessToken(jwt.serialize());
+        req = new UserInfoRequest(new URI("http://example.com"), token);
         setUserInfoRequest(req);
-        final Event event = action.execute(requestCtx);
+
+        event = action.execute(requestCtx);
         ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_GRANT);
     }
 
     /**
-     * Fails due token authz code is revoked. Test not 100% as it really does not test passing id to revocation cache.
+     * Fails due to access token being substituted with authorize code.
      * 
      * @throws NoSuchAlgorithmException 
      * @throws ComponentInitializationException 
@@ -199,13 +215,9 @@ public class ValidateAccessTokenTest extends BaseOIDCResponseActionTest {
      * @throws DataSealerException 
      */
     @Test
-    public void testFailsRevoked()
+    public void testFailsNotAccessToken()
             throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
-        action = new ValidateAccessToken();
-        action.setDataSealer(getDataSealer());
-        action.setRevocationCache(new MockRevocationCache(true, true));
-        action.initialize();
-        final TokenClaimsSet claims = new AccessTokenClaimsSet.Builder()
+        final TokenClaimsSet claims = new AuthorizeCodeClaimsSet.Builder()
                 .setJWTID(idGenerator)
                 .setClientID(new ClientID())
                 .setIssuer("issuer")
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SignUserInfoResponseTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/SignUserInfoResponseTest.java
similarity index 98%
rename from idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SignUserInfoResponseTest.java
rename to idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/SignUserInfoResponseTest.java
index c0be0f03..27e57542 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SignUserInfoResponseTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/SignUserInfoResponseTest.java
@@ -15,12 +15,13 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+package net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl;
 
 import java.net.URISyntaxException;
 import java.security.interfaces.ECPublicKey;
 import java.security.interfaces.RSAPublicKey;
 
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessTokenTest.java
index e2d4be55..c613e05f 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessTokenTest.java
@@ -19,25 +19,32 @@ package net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl;
 
 import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
-import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
 import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.NotBeforeClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.logic.BiFunctionSupport;
 import net.shibboleth.utilities.java.support.security.DataSealerException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.function.Function;
 
+import org.opensaml.profile.context.ProfileRequestContext;
 import org.springframework.webflow.execution.Event;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.id.ClientID;
-import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
-import com.nimbusds.openid.connect.sdk.UserInfoRequest;
 
 // Checkstyle: ThrowsCount OFF
 
@@ -50,25 +57,10 @@ public class ValidateAccessTokenTest extends BaseOIDCResponseActionTest {
     @BeforeMethod
     private void init() throws ComponentInitializationException, NoSuchAlgorithmException {
         action = new ValidateAccessToken();
-        action.setDataSealer(getDataSealer());
-        action.setRevocationCache(new MockRevocationCache(false, true));
+        action.setClaimsValidatorLookupStrategy(new ClaimsValidatorLookup());
         action.initialize();
     }
 
-    /**
-     * Test that action throws error if revocation cache is not set.
-     * 
-     * @throws NoSuchAlgorithmException 
-     * @throws ComponentInitializationException 
-     */
-    @Test(expectedExceptions = ComponentInitializationException.class)
-    public void testNoRevocationCache() throws NoSuchAlgorithmException, ComponentInitializationException {
-        action = new ValidateAccessToken();
-        action.setDataSealer(getDataSealer());
-        action.initialize();
-        action.execute(requestCtx);
-    }
-
     /**
      * Basic success case.
      * 
@@ -87,14 +79,13 @@ public class ValidateAccessTokenTest extends BaseOIDCResponseActionTest {
                 .setPrincipal("userPrin")
                 .setSubject("subject")
                 .setIssuedAt(Instant.now())
-                .setExpiresAt(Instant.now().plusSeconds(1))
+                .setExpiresAt(Instant.now().plusSeconds(300))
                 .setAuthenticationTime(Instant.now())
                 .setRedirectURI(new URI("http://example.com"))
                 .setScope(new Scope())
                 .build();
-        final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
-        final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
-        setUserInfoRequest(req);
+        respCtx.setAuthorizationGrantClaimsSet(claims);
+        
         final Event event = action.execute(requestCtx);
         ActionTestingSupport.assertProceedEvent(event);
     }
@@ -117,45 +108,14 @@ public class ValidateAccessTokenTest extends BaseOIDCResponseActionTest {
                 .setPrincipal("userPrin")
                 .setSubject("subject")
                 .setIssuedAt(Instant.now())
-                .setExpiresAt(Instant.now().plusSeconds(1))
+                .setExpiresAt(Instant.now().plusSeconds(300))
                 .setAuthenticationTime(Instant.now())
                 .setRedirectURI(new URI("http://example.com"))
                 .setScope(new Scope())
                 .setAudience(Collections.singletonList("foo"))
                 .build();
-        final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
-        final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
-        setUserInfoRequest(req);
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_GRANT);
-    }
-    
-    /**
-     * Fails due to access token being substituted with authorize code.
-     * 
-     * @throws NoSuchAlgorithmException 
-     * @throws ComponentInitializationException 
-     * @throws URISyntaxException 
-     * @throws DataSealerException 
-     */
-    @Test
-    public void testFailsNotAccessToken()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
-        final TokenClaimsSet claims = new AuthorizeCodeClaimsSet.Builder()
-                .setJWTID(idGenerator)
-                .setClientID(new ClientID())
-                .setIssuer("issuer")
-                .setPrincipal("userPrin")
-                .setSubject("subject")
-                .setIssuedAt(Instant.now())
-                .setExpiresAt(Instant.now().plusSeconds(1))
-                .setAuthenticationTime(Instant.now())
-                .setRedirectURI(new URI("http://example.com"))
-                .setScope(new Scope())
-                .build();
-        final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
-        final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
-        setUserInfoRequest(req);
+        respCtx.setAuthorizationGrantClaimsSet(claims);
+        
         final Event event = action.execute(requestCtx);
         ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_GRANT);
     }
@@ -178,50 +138,40 @@ public class ValidateAccessTokenTest extends BaseOIDCResponseActionTest {
                 .setPrincipal("userPrin")
                 .setSubject("subject")
                 .setIssuedAt(Instant.now())
-                .setExpiresAt(Instant.now().minusMillis(1))
+                .setExpiresAt(Instant.now().minusSeconds(120))
                 .setAuthenticationTime(Instant.now())
                 .setRedirectURI(new URI("http://example.com"))
                 .setScope(new Scope())
                 .build();
-        final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
-        final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
-        setUserInfoRequest(req);
+        respCtx.setAuthorizationGrantClaimsSet(claims);
+        
         final Event event = action.execute(requestCtx);
         ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_GRANT);
     }
 
-    /**
-     * Fails due token authz code is revoked. Test not 100% as it really does not test passing id to revocation cache.
-     * 
-     * @throws NoSuchAlgorithmException 
-     * @throws ComponentInitializationException 
-     * @throws URISyntaxException 
-     * @throws DataSealerException 
-     */
-    @Test
-    public void testFailsRevoked()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
-        action = new ValidateAccessToken();
-        action.setDataSealer(getDataSealer());
-        action.setRevocationCache(new MockRevocationCache(true, true));
-        action.initialize();
-        final TokenClaimsSet claims = new AccessTokenClaimsSet.Builder()
-                .setJWTID(idGenerator)
-                .setClientID(new ClientID())
-                .setIssuer("issuer")
-                .setPrincipal("userPrin")
-                .setSubject("subject")
-                .setIssuedAt(Instant.now())
-                .setExpiresAt(Instant.now().plusSeconds(1))
-                .setAuthenticationTime(Instant.now())
-                .setRedirectURI(new URI("http://example.com"))
-                .setScope(new Scope())
-                .build();
-        final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
-        final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
-        setUserInfoRequest(req);
-        final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_GRANT);
-    }
+    private class ClaimsValidatorLookup implements Function<ProfileRequestContext,ClaimsValidator> {
 
+        public ClaimsValidator apply(ProfileRequestContext t) {
+            final ChainingJWTClaimsValidator chain = new ChainingJWTClaimsValidator();
+            chain.setId("test");
+            chain.setRequireAll(true);
+            
+            final ArrayList<ClaimsValidator> validators = new ArrayList<>();
+            final RequiredClaimsValidator req = new RequiredClaimsValidator();
+            req.setRequiredClaims(Collections.singletonList("jti"));
+            validators.add(req);
+            validators.add(new NotBeforeClaimsValidator());
+            validators.add(new ExpiryClaimsValidator());
+            final AudienceClaimsValidator aud = new AudienceClaimsValidator();
+            aud.setAudienceLookupStrategy(BiFunctionSupport.constant("issuer"));
+            aud.setAllowMissing(true);
+            validators.add(aud);
+            
+            chain.setClaimValidators(validators);
+            
+            return chain;
+        }
+        
+    }
+    
 }
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index a163b99d..213f68ac 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,13 +9,13 @@
     </parent>
     <groupId>net.shibboleth.idp.plugin.oidc</groupId>
     <artifactId>idp-plugin-oidc-op-parent</artifactId>
-    <version>3.1.2-SNAPSHOT</version>
+    <version>3.2.0-SNAPSHOT</version>
     <name>Shibboleth IdP :: Plugins :: OpenID Connect Provider</name>
     <packaging>pom</packaging>
     <properties>
         <shib.idp.version>4.2.0</shib.idp.version>
         <opensaml.version>4.2.0</opensaml.version>
-        <oidc.common.version>2.0.0</oidc.common.version>
+        <oidc.common.version>2.0.1-SNAPSHOT</oidc.common.version>
         <gson.version>2.8.6</gson.version>
         <commons.io.version>2.6</commons.io.version>
         <checkstyle.configLocation>${project.basedir}/resources/checkstyle.xml</checkstyle.configLocation>

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


More information about the commits mailing list