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

Scott Cantor cantor.2 at osu.edu
Thu Jan 13 21:29:46 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=80578f441eb6ac9a16026e69bbcf4b0270c39abf

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

commit 80578f441eb6ac9a16026e69bbcf4b0270c39abf
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Thu Jan 13 16:29:42 2022 -0500

    JOIDC-11 - Support for client_credentials grant
    
    https://shibboleth.atlassian.net/browse/JOIDC-11
    
    Draft of initial implementation minus tests.
---
 .../idp/plugin/oidc/op/audit/AuditFields.java      |   6 +
 .../op/messaging/context/AccessTokenContext.java   | 114 ++++++
 .../context/OIDCAuthenticationResponseContext.java | 207 ++++++----
 .../context/OAuthAttributeResolutionContext.java   |  67 +++
 .../AccessTokenClaimsSetLookupFunction.java        |  72 ++++
 .../navigate/ValidatedAudienceLookupFunction.java  |  47 +++
 .../navigate/ValidatedScopeLookupFunction.java     |  47 +++
 .../oidc/op/profile/context/package-info.java      |  21 +
 .../oidc/op/token/support/TokenClaimsSet.java      |  88 +++-
 .../op/oauth2/profile/impl/BuildAccessToken.java   | 447 +++++++++++++++++++++
 .../OAuthAttributeResolutionContextDecorator.java  |  70 ++++
 .../impl/SetAccessTokenToResponseContext.java      | 116 ++++++
 .../op/oauth2/profile/impl/SignAccessToken.java    | 129 ++++++
 .../op/profile/impl/AddAttributesToClaimsSet.java  |  41 +-
 .../impl/SetAccessTokenToResponseContext.java      |   4 +-
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  21 +
 .../flows/oidc/abstract/oidc-abstract-beans.xml    |  13 +
 .../idp/flows/oidc/authorize/authorize-beans.xml   |   1 -
 .../idp/flows/oidc/token/token-beans.xml           | 122 ++++--
 .../shibboleth/idp/flows/oidc/token/token-flow.xml |  33 +-
 .../idp/flows/oidc/userinfo/userinfo-beans.xml     |  12 +-
 .../profile/impl/AddAttributesToClaimsSetTest.java |   1 -
 22 files changed, 1505 insertions(+), 174 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/audit/AuditFields.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/audit/AuditFields.java
index 394e56d4..474c837e 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/audit/AuditFields.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/audit/AuditFields.java
@@ -47,6 +47,12 @@ public final class AuditFields {
     /** The subject format (public/pairwise). */
     @Nonnull @NotEmpty public static final String SUB_FORMAT = SAMLAuditFields.NAMEID_FORMAT;
 
+    /** Token scope. */
+    @Nonnull @NotEmpty public static final String SCOPE = "scope";
+
+    /** Token audience. */
+    @Nonnull @NotEmpty public static final String AUDIENCE = "aud";
+
     /** The flag whether the id_token is encrypted. */
     @Nonnull @NotEmpty public static final String ENCRYPTED_ID_TOKEN = SAMLAuditFields.ENCRYPTION;
 
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/AccessTokenContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/AccessTokenContext.java
new file mode 100644
index 00000000..46121510
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/AccessTokenContext.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.messaging.context;
+
+import java.time.Duration;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import com.nimbusds.jwt.JWT;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+/**
+ * Subcontext carrying information used to produce access tokens.
+ * 
+ * @since 3.1.0
+ */
+public final class AccessTokenContext extends BaseContext {
+
+    /** Lifetime of the token. */
+    @Nullable private Duration lifetime;
+
+    /** Opaque token value if JWT format is not used. */
+    @Nullable @NotEmpty private String opaque;
+    
+    /** The signed/encrypted token in the case of JWT format. */
+    @Nullable private JWT jwt;
+
+    /**
+     * Get the token string in the case of an opaque token.
+     * 
+     * @return the token value
+     */
+    @Nullable @NotEmpty public String getOpaque() {
+        return opaque;
+    }
+
+    /**
+     * Set the token string in the case of an opaque token.
+     * 
+     * @param token the token string
+     * 
+     * @return this context
+     */
+    @Nonnull public AccessTokenContext setOpaque(@Nullable @NotEmpty final String token) {
+        opaque = StringSupport.trimOrNull(token);
+        return this;
+    }
+
+    /**
+     * Get the JWT in the case of a token in that form.
+     * 
+     * <p>May be in various states prior to signing/encryption.</p>
+     * 
+     * @return the JWT
+     */
+    @Nullable public JWT getJWT() {
+        return jwt;
+    }
+
+    /**
+     * Set the JWT in the case of a token in that form.
+     * 
+     * <p>May be in various states prior to signing/encryption.</p>
+     * 
+     * @param token the JWT
+     * 
+     * @return this context
+     */
+    @Nonnull public AccessTokenContext setJWT(@Nullable final JWT token) {
+        jwt = token;
+        return this;
+    }
+    
+    /**
+     * Get the token lifetime.
+     * 
+     * @return lifetime
+     */
+    @Nullable public Duration getLifetime() {
+        return lifetime;
+    }
+    
+    /**
+     * Set the token lifetime.
+     * 
+     * @param lt lifetime
+     * 
+     * @return this context
+     */
+    @Nonnull public AccessTokenContext setLifetime(@Nullable final Duration lt) {
+        lifetime = lt;
+        return this;
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
index 0e01d22a..94e54224 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
@@ -20,6 +20,8 @@ package net.shibboleth.idp.plugin.oidc.op.messaging.context;
 import java.net.URI;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -33,11 +35,14 @@ import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
 import com.nimbusds.oauth2.sdk.token.RefreshToken;
 import com.nimbusds.openid.connect.sdk.OIDCClaimsRequest;
 import com.nimbusds.openid.connect.sdk.claims.ACR;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
 import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
 import com.nimbusds.openid.connect.sdk.claims.UserInfo;
 
 import net.shibboleth.idp.attribute.AttributesMapContainer;
 import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.utilities.java.support.annotation.constraint.Live;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 
 /**
  * Subcontext carrying information to form authentication, token and userinfo responses for relying party. This context
@@ -57,6 +62,10 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
     @Nullable
     private UserInfo userInfo;
 
+    /** The access token claim set. */
+    @Nullable
+    private ClaimsSet accessTokenClaimsSet;
+    
     /** The signed/encrypted id token / user info response formed. */
     @Nullable
     private JWT processedToken;
@@ -75,7 +84,11 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
 
     /** Validated scope values. */
     @Nullable
-    private Scope requestedScope;
+    private Scope validatedScope;
+    
+    /** Validated audience values. */
+    @Nonnull @NonnullElements
+    private List<String> validatedAudience;
 
     /** Requested sub value. */
     @Nullable
@@ -115,6 +128,11 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
     /** Mapped requested claims from the Userinfo set. */
     @Nullable private AttributesMapContainer mappedUserinfoRequestedClaims;
     
+    /** Constructor. */
+    public OIDCAuthenticationResponseContext() {
+        validatedAudience = new ArrayList<>();
+    }
+    
     /**
      * Get requested claims.
      * 
@@ -241,60 +259,6 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
         authorizationCode = code == null ? null : new AuthorizationCode(code);
     }
 
-    /**
-     * Get access token.
-     * 
-     * @return access token
-     */
-    @Nullable
-    public AccessToken getAccessToken() {
-        return accessToken;
-    }
-
-    /**
-     * Set access token.
-     * 
-     * @param token String to form access token
-     * @param lifeTime lifetime of the access token
-     */
-    public void setAccessToken(@Nullable final String token, @Nonnull final Duration lifeTime) {
-        setAccessToken(token, lifeTime, null);
-    }
-
-
-    /**
-     * Set access token.
-     * 
-     * @param token string to form access token
-     * @param lifeTime lifetime of the access token
-     * @param scope scope of the access token
-     * 
-     * @since 3.1.0
-     */
-    public void setAccessToken(@Nullable final String token, @Nonnull final Duration lifeTime,
-            @Nonnull final Scope scope) {
-        accessToken = token == null ? null : new BearerAccessToken(token, lifeTime.getSeconds(), scope);
-    }
-    
-    /**
-     * Get refresh token.
-     * 
-     * @return refresh token
-     */
-    @Nullable
-    public RefreshToken getRefreshToken() {
-        return refreshToken;
-    }
-
-    /**
-     * Set refresh token.
-     * 
-     * @param token String to form refresh token
-     */
-    public void setRefreshToken(@Nullable final String token) {
-        refreshToken = token == null ? null : new RefreshToken(token);
-    }
-
     /**
      * Gets requested sub value.
      * 
@@ -311,7 +275,7 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
      * @param sub requested sub value.
      */
     public void setRequestedSubject(@Nullable final String sub) {
-        this.requestedSubject = sub;
+        requestedSubject = sub;
     }
 
     /**
@@ -319,8 +283,7 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
      * 
      * @return Name ID generated for response
      */
-    @Nullable
-    public String getSubject() {
+    @Nullable public String getSubject() {
         return subject;
     }
 
@@ -338,8 +301,7 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
      * 
      * @return generated subject type.
      */
-    @Nullable
-    public String getSubjectType() {
+    @Nullable public String getSubjectType() {
         return subjectType;
     }
 
@@ -357,9 +319,8 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
      * 
      * @return validated scope values
      */
-    @Nullable
-    public Scope getScope() {
-        return requestedScope;
+    @Nullable public Scope getScope() {
+        return validatedScope;
     }
 
     /**
@@ -368,7 +329,18 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
      * @param scope scope values
      */
     public void setScope(@Nullable final Scope scope) {
-        requestedScope = scope;
+        validatedScope = scope;
+    }
+
+    /**
+     * Get modifiable collection of token audience values.
+     * 
+     * @return audience collection
+     * 
+     * @since 3.1.0
+     */
+    @Nonnull @NonnullElements @Live public List<String> getAudience() {
+        return validatedAudience;
     }
 
     /**
@@ -434,9 +406,9 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
     }
 
     /**
-     * Get the id token.
+     * Get the {@link IDTokenClaimsSet} object that will source the ID token.
      * 
-     * @return The id token.
+     * @return ID token claims set
      */
     @Nullable
     public IDTokenClaimsSet getIDToken() {
@@ -444,18 +416,18 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
     }
 
     /**
-     * Set the id token.
+     * Set the {@link IDTokenClaimsSet} object that will source the ID token.
      * 
-     * @param token The id token.
+     * @param token ID token claims set
      */
     public void setIDToken(@Nullable final IDTokenClaimsSet token) {
         idToken = token;
     }
 
     /**
-     * Get the user info.
+     * Get the {@link UserInfo} claims set that will source the UserInfo response.
      * 
-     * @return The user info.
+     * @return UserInfo claims set
      */
     @Nullable
     public UserInfo getUserInfo() {
@@ -463,18 +435,94 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
     }
 
     /**
-     * Set the user info.
+     * Set the {@link UserInfo} claims set that will source the UserInfo response.
      * 
-     * @param info The user info.
+     * @param info UserInfo claims set
      */
     public void setUserInfo(@Nullable final UserInfo info) {
         userInfo = info;
     }
-
-    /**
-     * Get the signed/encrypted id token / user info response.
-     * 
-     * @return The signed id token / user info response
+    
+   /**
+    * Get the access token claims set (used when prepping OAuth-only access tokens).
+    * 
+    * @return access token claims set
+    * 
+    * @since 3.1.0
+    */
+   @Nullable
+   public ClaimsSet getAccessTokenClaimSet() {
+       return accessTokenClaimsSet;
+   }
+
+   /**
+    * Set the access token claims set (used when prepping OAuth-only access tokens).
+    * 
+    * @param claims access token claims set
+    * 
+    * @since 3.1.0
+    */
+   public void setAccessTokenClaimsSet(@Nullable final ClaimsSet claims) {
+       accessTokenClaimsSet = claims;
+   }
+
+   /**
+    * Get access token.
+    * 
+    * @return access token
+    */
+   @Nullable
+   public AccessToken getAccessToken() {
+       return accessToken;
+   }
+
+   /**
+    * Set access token.
+    * 
+    * @param token String to form access token
+    * @param lifeTime lifetime of the access token
+    */
+   public void setAccessToken(@Nullable final String token, @Nonnull final Duration lifeTime) {
+       setAccessToken(token, lifeTime, null);
+   }
+
+   /**
+    * Set access token.
+    * 
+    * @param token string to form access token
+    * @param lifeTime lifetime of the access token
+    * @param scope scope of the access token
+    * 
+    * @since 3.1.0
+    */
+   public void setAccessToken(@Nullable final String token, @Nonnull final Duration lifeTime,
+           @Nonnull final Scope scope) {
+       accessToken = token == null ? null : new BearerAccessToken(token, lifeTime.getSeconds(), scope);
+   }
+   
+   /**
+    * Get refresh token.
+    * 
+    * @return refresh token
+    */
+   @Nullable
+   public RefreshToken getRefreshToken() {
+       return refreshToken;
+   }
+
+   /**
+    * Set refresh token.
+    * 
+    * @param token String to form refresh token
+    */
+   public void setRefreshToken(@Nullable final String token) {
+       refreshToken = token == null ? null : new RefreshToken(token);
+   }
+   
+    /**
+     * Get the signed/encrypted ID token / UserInfo response JWT.
+     * 
+     * @return ID token / UserInfo response JWT
      */
     @Nullable
     public JWT getProcessedToken() {
@@ -482,9 +530,9 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
     }
 
     /**
-     * Set the signed/encrypted id token / user info response.
+     * Set the signed/encrypted ID token / UserInfo response JWT.
      * 
-     * @param token The signed id token / user info response
+     * @param token ID token / UserInfo response JWT
      */
     public void setProcessedToken(@Nullable final JWT token) {
         processedToken = token;
@@ -508,4 +556,5 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
     public void setRequestObject(@Nullable final JWT obj) {
         requestObject = obj;
     }
+    
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/OAuthAttributeResolutionContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/OAuthAttributeResolutionContext.java
new file mode 100644
index 00000000..0bcf6d2f
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/OAuthAttributeResolutionContext.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.context;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.Live;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+
+/**
+ * A supplemental context adding OAuth- and OIDC-related input to the attribute resolution process.
+ * 
+ * @since 3.1.0
+ */
+public final class OAuthAttributeResolutionContext extends BaseContext {
+
+    /** Requested and validated scopes. */
+    @Nonnull @NonnullElements private final Collection<String> scopes;
+
+    /** Requested resources. */
+    @Nonnull @NonnullElements private final Collection<String> resources;
+    
+    /** Constructor. */
+    public OAuthAttributeResolutionContext() {
+        scopes = new ArrayList<>();
+        resources = new ArrayList<>();
+    }
+    
+    /**
+     * Get the requested and metadata-validated scope collection for this request.
+     * 
+     * @return modifiable collection of scope values
+     */
+    @Nonnull @NonnullElements @Live public Collection<String> getScope() {
+        return scopes;
+    }
+
+    /**
+     * Get the requested audience collection for this request.
+     * 
+     * @return modifiable collection of audience values
+     */
+    @Nonnull @NonnullElements @Live public Collection<String> getResources() {
+        return resources;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AccessTokenClaimsSetLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AccessTokenClaimsSetLookupFunction.java
new file mode 100644
index 00000000..7340d847
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AccessTokenClaimsSetLookupFunction.java
@@ -0,0 +1,72 @@
+/*
+ * 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 javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.opensaml.profile.context.ProfileRequestContext;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+
+/**
+ * A function that returns the access token claims set from response context.
+ * 
+ * <p>An option exists to create it if it does not already exist.</p>
+ * 
+ * @since 3.1.0
+ */
+public class AccessTokenClaimsSetLookupFunction
+        implements ContextDataLookupFunction<ProfileRequestContext,ClaimsSet> {
+
+    /** Whether to create the claims set if it is absent. */
+    private boolean autocreate;
+    
+    /**
+     * Sets whether to create the {@link  ClaimsSet} if absent.
+     * 
+     * <p>Defaults to false.</p>
+     * 
+     * @param flag flag to set
+     */
+    public void setAutoCreate(final boolean flag) {
+        autocreate = flag;
+    }
+    
+    /** {@inheritDoc} */
+    @Nullable
+    public ClaimsSet apply(@Nullable final ProfileRequestContext input) {
+        if (input == null || input.getOutboundMessageContext() == null) {
+            return null;
+        }
+        
+        final OIDCAuthenticationResponseContext ctx =
+                input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
+        if (ctx == null) {
+            return null;
+        }
+        
+        if (ctx.getAccessTokenClaimSet() == null && autocreate) {
+            ctx.setAccessTokenClaimsSet(new ClaimsSet());
+        }
+        
+        return ctx.getAccessTokenClaimSet();
+    }
+
+}
\ 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/ValidatedAudienceLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ValidatedAudienceLookupFunction.java
new file mode 100644
index 00000000..444acd45
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ValidatedAudienceLookupFunction.java
@@ -0,0 +1,47 @@
+/*
+ * 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.util.Collection;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+
+/** A function that returns {@link OIDCAuthenticationResponseContext#getAudience()}. */
+public class ValidatedAudienceLookupFunction
+        implements ContextDataLookupFunction<ProfileRequestContext,Collection<String>> {
+
+    /** {@inheritDoc} */
+    @Nullable
+    public Collection<String> apply(@Nullable final ProfileRequestContext input) {
+        if (input == null || input.getOutboundMessageContext() == null) {
+            return null;
+        }
+        final OIDCAuthenticationResponseContext ctx =
+                input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
+        if (ctx == null) {
+            return null;
+        }
+        return ctx.getAudience();
+    }
+
+}
\ 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/ValidatedScopeLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ValidatedScopeLookupFunction.java
new file mode 100644
index 00000000..3c344da6
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ValidatedScopeLookupFunction.java
@@ -0,0 +1,47 @@
+/*
+ * 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 javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.oauth2.sdk.Scope;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+
+/** A function that returns {@link OIDCAuthenticationResponseContext#getScope()}. */
+public class ValidatedScopeLookupFunction
+        implements ContextDataLookupFunction<ProfileRequestContext,Scope> {
+
+    /** {@inheritDoc} */
+    @Nullable
+    public Scope apply(@Nullable final ProfileRequestContext input) {
+        if (input == null || input.getOutboundMessageContext() == null) {
+            return null;
+        }
+        final OIDCAuthenticationResponseContext ctx =
+                input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
+        if (ctx == null) {
+            return null;
+        }
+        return ctx.getScope();
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/package-info.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/package-info.java
new file mode 100644
index 00000000..7fa5371a
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Contexts related to OP profile actions.
+ */
+package net.shibboleth.idp.plugin.oidc.op.profile.context;
\ No newline at end of file
diff --git a/idp-oidc-extension-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 cbbf4196..ea9b6e5e 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
@@ -20,6 +20,7 @@ package net.shibboleth.idp.plugin.oidc.op.token.support;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -82,7 +83,7 @@ public class TokenClaimsSet {
     @Nonnull @NotEmpty public static final String KEY_EXPIRATION_TIME = "exp";
 
     /** Not before time of the token. */
-    @Nonnull @NotEmpty public static final String KEY_NOTBEFORE_TIME = "nbt";
+    @Nonnull @NotEmpty public static final String KEY_NOTBEFORE_TIME = "nbf";
     
     /** Issue time of the token. */
     @Nonnull @NotEmpty public static final String KEY_ISSUED_AT = "iat";
@@ -173,9 +174,6 @@ public class TokenClaimsSet {
         if (tokenClaimsSet.getStringClaim(KEY_ISSUER) == null) {
             throw new ParseException("claim iss must exist and not be null", 0);
         }
-        if (tokenClaimsSet.getStringClaim(KEY_USER_PRINCIPAL) == null) {
-            throw new ParseException("claim prncpl must exist and not be null", 0);
-        }
         if (tokenClaimsSet.getStringClaim(KEY_SUBJECT) == null) {
             throw new ParseException("claim sub must exist and not be null", 0);
         }
@@ -194,9 +192,6 @@ public class TokenClaimsSet {
         if (tokenClaimsSet.getDateClaim(KEY_AUTH_TIME) == null) {
             throw new ParseException("claim auth_time must exist and not be null", 0);
         }
-        if (tokenClaimsSet.getStringClaim(KEY_REDIRECT_URI) == null) {
-            throw new ParseException("claim redirect_uri must exist and not be null", 0);
-        }
         if (tokenClaimsSet.getStringClaim(KEY_SCOPE) == null) {
             throw new ParseException("claim scope must exist and not be null", 0);
         }
@@ -306,8 +301,8 @@ public class TokenClaimsSet {
     public boolean isTimeValid() {
         final Instant now = Instant.now();
         if (getExp().isAfter(now)) {
-            final Instant nbt = getNotBefore();
-            return nbt == null || now == nbt || now.isAfter(nbt);
+            final Instant nbf = getNotBefore();
+            return nbf == null || now == nbf || now.isAfter(nbf);
         }
         
         return false;
@@ -654,9 +649,13 @@ public class TokenClaimsSet {
         /** Code challenge. */
         @Nullable protected String codeChallenge;
         
+        /** Extends the token with custom claims. */
+        @Nonnull protected Map<String,Object> customClaims;
+        
         /** Default constructor. */
         protected Builder() {
             audience = Collections.emptyList();
+            customClaims = new HashMap<>();
         }
         
 // Checkstyle:CyclomaticComplexity OFF
@@ -673,13 +672,13 @@ public class TokenClaimsSet {
          */
         @Nonnull protected JWTClaimsSet buildJWTClaimsSet(@Nonnull @NotEmpty final String tokenType) {
             
-            if (tokenType == null || jwtid == null || rpId == null || iss == null || principal == null
-                    || iat == null || exp == null || authTime == null || redirect == null || reqScope == null
+            if (tokenType == null || jwtid == null || rpId == null || iss == null
+                    || iat == null || exp == null || authTime == null || reqScope == null
                     || sub == null) {
                 throw new RuntimeException("Invalid parameters, programming error");
             }
             
-            return new JWTClaimsSet.Builder()
+            final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder()
                     .claim(KEY_TYPE, tokenType)
                     .jwtID(jwtid)
                     .claim(KEY_CLIENTID, rpId.getValue())
@@ -701,8 +700,15 @@ public class TokenClaimsSet {
                     .claim(KEY_DELIVERY_CLAIMS_USERINFO, dlClaimsUI == null ? null : dlClaimsUI.toJSONObject())
                     .claim(KEY_CONSENTED_CLAIMS, consentedClaims)
                     .claim(KEY_CODE_CHALLENGE, codeChallenge)
-                    .claim(KEY_CONSENT_ENABLED, consentEnabled)
-                    .build();
+                    .claim(KEY_CONSENT_ENABLED, consentEnabled);
+            
+            customClaims.forEach((n,v) -> {
+                if (n != null) {
+                    builder.claim(n, v);
+                }
+            });
+            
+            return builder.build();
         }
 // Checkstyle:CyclomaticComplexity ON
         
@@ -922,12 +928,29 @@ public class TokenClaimsSet {
          * @param claimsRequest claims request of the authentication request.
          * 
          * @return the builder
+         * 
+         * @deprecated
          */
+        @Deprecated(since="3.1.0", forRemoval=true)
         public Builder<T> setClaims(@Nullable final OIDCClaimsRequest claimsRequest) {
             reqClaims = claimsRequest;
             return this;
         }
 
+        /**
+         * Set claims request of the authentication request.
+         * 
+         * @param claimsRequest claims request of the authentication request.
+         * 
+         * @return the builder
+         * 
+         * @since 3.1.0
+         */
+        public Builder<T> setClaimsRequest(@Nullable final OIDCClaimsRequest claimsRequest) {
+            reqClaims = claimsRequest;
+            return this;
+        }
+
         /**
          * Set token delivery claims delivered both for id token and userinfo response.
          * 
@@ -999,7 +1022,44 @@ public class TokenClaimsSet {
             codeChallenge = challenge;
             return this;
         }
+        
+        /**
+         * Add a custom claim.
+         * 
+         * <p>This method does NOT check for overlap with existing claim names.</p>
+         * 
+         * @param name claim name
+         * @param value claim value
+         * 
+         * @return this builder
+         * 
+         * @since 3.1.0
+         */
+        public Builder<T> addCustomClaim(@Nonnull @NotEmpty final String name, @Nullable final Object value) {
+            customClaims.put(name, value);
+            return this;
+        }
 
+        /**
+         * Sets a batch of custom claim from a {@link JSONObject}.
+         * 
+         * <p>This method does NOT check for overlap with existing claim names.</p>
+         * 
+         * @param claims the claims
+         * 
+         * @return this builder
+         * 
+         * @since 3.1.0
+         */
+        public Builder<T> setCustomClaims(@Nonnull final JSONObject claims) {
+            claims.forEach((n,v) -> {
+                if (n != null) {
+                    customClaims.put(n, v);
+                }
+            });
+            return this;
+        }
+        
         /**
          * Builds claims set.
          * 
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
new file mode 100644
index 00000000..fa170cb6
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
@@ -0,0 +1,447 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
+
+import java.net.URI;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.context.AttributeContext;
+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.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet.Builder;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.idp.profile.context.navigate.ResponderIdLookupFunction;
+import net.shibboleth.oidc.profile.config.navigate.AccessTokenLifetimeLookupFunction;
+import net.shibboleth.oidc.profile.config.navigate.AccessTokenTypeLookupFunction;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.security.DataSealer;
+import net.shibboleth.utilities.java.support.security.DataSealerException;
+import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrategy;
+import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifierGenerationStrategy;
+
+/**
+ * Action that creates an Access Token, and stores it to an {@link AccessTokenContext}.
+ * 
+ * <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>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#MESSAGE_PROC_ERROR}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link IdPEventIds#INVALID_ATTRIBUTE_CTX}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ * @event {@link IdPEventIds#INVALID_SUBJECT_CTX}
+ * 
+ * @since 3.1.0
+ */
+public class BuildAccessToken extends AbstractOIDCResponseAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(BuildAccessToken.class);
+
+    /** Sealer to use for opaque tokens. */
+    @Nullable private DataSealer dataSealer;
+    
+    /** Strategy used to obtain the response issuer value. */
+    @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
+
+    /** Strategy used to obtain the access token type to issue. */
+    @Nonnull private Function<ProfileRequestContext,String> accessTokenTypeLookupStrategy;    
+    
+    /** Strategy used to obtain the access token lifetime. */
+    @Nonnull private Function<ProfileRequestContext,Duration> accessTokenLifetimeLookupStrategy;
+
+    /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
+    @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
+
+    /** Strategy used to create the subcontext to hold the token. */
+    @Nonnull private Function<ProfileRequestContext,AccessTokenContext> accessTokenContextCreationStrategy;
+
+    /** Strategy used to locate the attribute source for scope/audience. */
+    @Nonnull private Function<ProfileRequestContext,AttributeContext> attributeContextLookupStrategy;
+    
+    /** Source scope/audience from unfiltered attributes. */
+    private boolean useUnfilteredAttributes;
+    
+    /** ID of attribute to populate scope from. */
+    @Nullable @NotEmpty private String scopeAttribute;
+    
+    /** ID of attribute to populate audience from. */
+    @Nullable @NotEmpty private String audienceAttribute;
+    
+    /** Use a JWT for the token. */
+    private boolean jwtTokenType;
+    
+    /** Access token context. */
+    @Nullable private AccessTokenContext accessTokenCtx;
+    
+    /** Attribute context. */
+    @Nullable private AttributeContext attributeCtx;
+
+    /** Subject context. */
+    @Nullable private SubjectContext subjectCtx;
+
+    /** The generator to use. */
+    @Nullable private IdentifierGenerationStrategy idGenerator;
+    
+    /** Token request. */
+    @Nullable private TokenRequest tokenRequest;
+    
+    /** Constructor. */
+    public BuildAccessToken() {
+        accessTokenTypeLookupStrategy = new AccessTokenTypeLookupFunction();
+        accessTokenLifetimeLookupStrategy = new AccessTokenLifetimeLookupFunction();
+        issuerLookupStrategy = new ResponderIdLookupFunction();
+        idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
+        
+        // PRC -> inbound message context -> OIDC response context -> ATC
+        accessTokenContextCreationStrategy = new ChildContextLookup<>(AccessTokenContext.class, true).compose(
+                new ChildContextLookup<>(OIDCAuthenticationResponseContext.class).compose(
+                        new InboundMessageContextLookup()));
+        
+        // PRC -> RPC -> AC
+        attributeContextLookupStrategy = new ChildContextLookup<>(AttributeContext.class).compose(
+                new ChildContextLookup<>(RelyingPartyContext.class));
+        
+        useUnfilteredAttributes = true;
+        scopeAttribute = "scope";
+        audienceAttribute = "audience";
+    }
+    
+    /**
+     * Set {@lik DataSealer} to use for opaque tokens.
+     * 
+     * @param sealer sealer to use for opaque tokens
+     */
+    public void setDataSealer(@Nullable final DataSealer sealer) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        dataSealer = sealer;
+    }
+    
+   /**
+    * Set the strategy used to obtain the access token type.
+    * 
+    * @param strategy lookup strategy
+    */
+    public void setAccessTokenTypeLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+       
+        accessTokenTypeLookupStrategy =
+                Constraint.isNotNull(strategy, "Access token type 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 create the {@link AccessTokenContext} to use.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setAccessTokenContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,AccessTokenContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        accessTokenContextCreationStrategy =
+                Constraint.isNotNull(strategy, "AccessTokenContext creation strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the {@link AttributeContext} associated with a given
+     * {@link ProfileRequestContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAttributeContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, AttributeContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        attributeContextLookupStrategy =
+                Constraint.isNotNull(strategy, "AttributeContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to locate the issuer value to use.
+     * 
+     * @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 whether to source the scope and audience claims from unfiltered attributes.
+     * 
+     * <p>Default is true.</p>
+     * 
+     * @param flag flag to set
+     */
+    public void setUseUnfilteredAttributes(final boolean flag) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        useUnfilteredAttributes = flag;
+    }
+    
+    /**
+     * Set the ID of an {@link IdPAttribute} to source the scope claim.
+     * 
+     * <p>If unset or no values can be extracted, the validated scopes from the request are used.</p>
+     * 
+     * @param id attribute ID
+     */
+    public void setScopeAttribute(@Nullable @NotEmpty final String id) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        scopeAttribute = StringSupport.trimOrNull(id);
+    }
+
+    /**
+     * Set the ID of an {@link IdPAttribute} to source the audience claim.
+     * 
+     * <p>If set, an attribute must be available to pull the values from. If unset, the requested resource
+     * values are used.</p>
+     * 
+     * @param id attribute ID
+     */
+    public void setAudienceAttribute(@Nullable @NotEmpty final String id) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        audienceAttribute = StringSupport.trimOrNull(id);
+    }
+
+// Checkstyle: CyclomaticComplexity OFF
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        if (profileRequestContext.getInboundMessageContext().getMessage() instanceof TokenRequest) {
+            tokenRequest = (TokenRequest) profileRequestContext.getInboundMessageContext().getMessage();
+        } else {
+            log.error("{} No inbound TokenRequest message", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        
+        final String tokenType = accessTokenTypeLookupStrategy.apply(profileRequestContext);
+        jwtTokenType = tokenType != null && "JWT".equals(tokenType);
+        
+        
+        subjectCtx = profileRequestContext.getSubcontext(SubjectContext.class);
+        if (subjectCtx == null) {
+            log.error("{} No SubjectContext located", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_SUBJECT_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;
+        }
+
+        accessTokenCtx = accessTokenContextCreationStrategy.apply(profileRequestContext);
+        if (accessTokenCtx == null) {
+            log.error("{} Unable to create AccessTokenContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        attributeCtx = attributeContextLookupStrategy.apply(profileRequestContext);
+        if (attributeCtx == null && (scopeAttribute != null || audienceAttribute != null)) {
+            log.error("{} Unable to locate AttributeContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_ATTRIBUTE_CTX);
+            return false;
+        }
+        
+        final Duration lifetime = accessTokenLifetimeLookupStrategy.apply(profileRequestContext);
+        if (lifetime == null) {
+            log.error("{} No lifetime supplied for access token", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
+        accessTokenCtx.setLifetime(lifetime);
+        
+        return true;
+    }
+// Checkstyle: CyclomaticComplexity ON
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final OIDCAuthenticationResponseContext ctx = getOidcResponseContext();
+
+        final Collection<String> audience = getAudience();
+        if (audience == null || audience.isEmpty()) {
+            log.warn("{} Unable to determine audience value, failing request", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
+            return;
+        }
+        ctx.getAudience().addAll(audience);
+        log.debug("{} Building access token with audience: {}", getLogPrefix(), audience);
+
+        final Scope scope = getScope(ctx.getScope());
+        log.debug("{} Building access token with scope: {}", getLogPrefix(), scope);
+
+        final Instant dateExp = Instant.now().plus(accessTokenCtx.getLifetime());
+        
+        final AccessTokenClaimsSet.Builder builder = (Builder) new AccessTokenClaimsSet.Builder()
+                .setJWTID(idGenerator)
+                .setClientID(tokenRequest.getClientID())
+                .setIssuer(issuerLookupStrategy.apply(profileRequestContext))
+                .setSubject(subjectCtx.getPrincipalName())
+                .setIssuedAt(Instant.now())
+                .setExpiresAt(dateExp)
+                .setACR(ctx.getAcr())
+                .setAuthenticationTime(ctx.getAuthTime())
+                .setScope(scope)
+                .setAudience(audience);
+        
+        if (ctx.getAccessTokenClaimSet() != null) {
+            builder.setCustomClaims(ctx.getAccessTokenClaimSet().toJSONObject());
+        }
+        
+        final AccessTokenClaimsSet claimsSet = builder.build();
+        
+        try {
+            if (jwtTokenType) {
+                accessTokenCtx.setJWT(new PlainJWT(claimsSet.getClaimsSet()));
+                log.debug("{} Claims stored to JWT access token: {}", getLogPrefix(), claimsSet.serialize(),
+                        accessTokenCtx.getJWT());
+            } else { 
+                accessTokenCtx.setOpaque(claimsSet.serialize(dataSealer));
+                log.debug("{} Claims '{}' converted to opaque access token: {}", getLogPrefix(), claimsSet.serialize(),
+                        accessTokenCtx.getOpaque());
+            }
+        } catch (final DataSealerException e) {
+            log.error("{} Access Token wrapping failed: {}", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
+        }
+    }
+    
+    /**
+     * Produce the desired {@link Scope} claim for the access token.
+     * 
+     * @param validatedScope requested scope(s) that are valid according to client's metadata
+     * 
+     * @return derived {@link Scope} to use or null
+     */
+    @Nullable private Scope getScope(@Nullable final Scope validatedScope) {
+        
+        if (scopeAttribute != null) {
+            final IdPAttribute source = (useUnfilteredAttributes ? attributeCtx.getUnfilteredIdPAttributes()
+                    : attributeCtx.getIdPAttributes()).get(scopeAttribute);
+            if (source != null) {
+                return Scope.parse(
+                        source.getValues().stream()
+                            .filter(StringAttributeValue.class::isInstance)
+                            .map(StringAttributeValue.class::cast)
+                            .map(StringAttributeValue::getValue)
+                            .collect(Collectors.toUnmodifiableList()));
+            }
+            
+            log.warn("{} No source attribute {} available to produce scope claim", getLogPrefix(), scopeAttribute);
+        }
+        
+        log.debug("{} Using originally requested/validated scope", getLogPrefix());
+        return validatedScope;
+    }
+    
+    @Nullable @NonnullElements private Collection<String> getAudience() {
+        if (audienceAttribute != null) {
+            final IdPAttribute source = (useUnfilteredAttributes ? attributeCtx.getUnfilteredIdPAttributes()
+                    : attributeCtx.getIdPAttributes()).get(audienceAttribute);
+            if (source != null) {
+                return source.getValues().stream()
+                        .filter(StringAttributeValue.class::isInstance)
+                        .map(StringAttributeValue.class::cast)
+                        .map(StringAttributeValue::getValue)
+                        .collect(Collectors.toUnmodifiableList());
+            }
+        }
+        
+        log.debug("{} Using originally requested resource(s) as audience", getLogPrefix());
+        return tokenRequest.getResources().stream().map(URI::toString).collect(Collectors.toUnmodifiableList());
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/OAuthAttributeResolutionContextDecorator.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/OAuthAttributeResolutionContextDecorator.java
new file mode 100644
index 00000000..b031350f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/OAuthAttributeResolutionContextDecorator.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.oauth2.sdk.TokenRequest;
+
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.OAuthAttributeResolutionContext;
+
+/**
+ * Adds OAuth-specific details about the resolution request to the tree.
+ * 
+ * @since 3.1.0
+ */
+public class OAuthAttributeResolutionContextDecorator implements Consumer<AttributeResolutionContext> {
+
+    /** {@inheritDoc} */
+    @Override
+    public void accept(@Nullable final AttributeResolutionContext input) {
+        
+        final OAuthAttributeResolutionContext oauthCtx =
+                input.getSubcontext(OAuthAttributeResolutionContext.class, true);
+        
+        if (input != null && input.getParent() instanceof ProfileRequestContext) {
+            final MessageContext inbound = ((ProfileRequestContext) input.getParent()).getInboundMessageContext();
+            if (inbound != null && inbound.getMessage() instanceof TokenRequest) {
+                final Collection<URI> resources = ((TokenRequest) inbound.getMessage()).getResources();
+                if (resources != null) {
+                    oauthCtx.getResources().addAll(
+                            resources.stream().map(URI::toString).collect(Collectors.toUnmodifiableList()));
+                }
+            }
+            
+            final MessageContext outbound = ((ProfileRequestContext) input.getParent()).getOutboundMessageContext();
+            if (outbound != null) {
+                final OIDCAuthenticationResponseContext responseCtx =
+                        outbound.getSubcontext(OIDCAuthenticationResponseContext.class);
+                if (responseCtx != null && responseCtx.getScope() != null) {
+                    oauthCtx.getScope().addAll(responseCtx.getScope().toStringList());
+                }
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
new file mode 100644
index 00000000..a18660b8
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.AccessTokenContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Action that creates a Access Token, and sets it to work context
+ * {@link OIDCAuthenticationResponseContext#getAccessToken()} located under
+ * {@link ProfileRequestContext#getOutboundMessageContext()}.
+ */
+public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(SetAccessTokenToResponseContext.class);
+
+    /** Strategy used to locate the subcontext with the token. */
+    @Nonnull private Function<ProfileRequestContext,AccessTokenContext> accessTokenContextLookupStrategy;
+    
+    /** Token context. */
+    @Nullable private AccessTokenContext tokenCtx; 
+    
+    /** Constructor. */
+    public SetAccessTokenToResponseContext() {
+        // PRC -> inbound message context -> OIDC response context -> ATC
+        accessTokenContextLookupStrategy = new ChildContextLookup<>(AccessTokenContext.class, true).compose(
+                new ChildContextLookup<>(OIDCAuthenticationResponseContext.class).compose(
+                        new InboundMessageContextLookup()));
+    }
+
+    /**
+     * 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);
+        
+        accessTokenContextLookupStrategy =
+                Constraint.isNotNull(strategy, "AccessTokenContext creation strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        tokenCtx = accessTokenContextLookupStrategy.apply(profileRequestContext);
+        if (tokenCtx == null) {
+            log.error("{} AccessTokenContext is missing", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        
+        if (tokenCtx.getJWT() == null && tokenCtx.getOpaque() == null) {
+            log.debug("{} Access token is missing", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        final String token;
+        if (tokenCtx.getJWT() != null) {
+            token = tokenCtx.getJWT().serialize();
+        } else {
+            token = tokenCtx.getOpaque();
+        }
+        
+        getOidcResponseContext().setAccessToken(token, tokenCtx.getLifetime(), getOidcResponseContext().getScope());
+    }
+
+}
\ 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/SignAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SignAccessToken.java
new file mode 100644
index 00000000..ddbebd8a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SignAccessToken.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
+
+import java.text.ParseException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.AccessTokenContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractSignJWTAction;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Action that signs {@link AccessTokenContext#getJWT()} and overwrites it with the signed version.
+ * 
+ * <p>The action exits gracefully if no signing parameters exist or the JWT does not exist.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#MESSAGE_PROC_ERROR}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ */
+public class SignAccessToken extends AbstractSignJWTAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(SignAccessToken.class);
+
+    /** Strategy used to locate the subcontext with the token. */
+    @Nonnull private Function<ProfileRequestContext,AccessTokenContext> accessTokenContextLookupStrategy;
+    
+    /** Token context. */
+    @Nullable private AccessTokenContext tokenCtx; 
+    
+    /** Source of claims set to sign. */
+    @Nullable private JWTClaimsSet claimsSet;
+
+    /** Constructor. */
+    public SignAccessToken() {
+        // PRC -> inbound message context -> OIDC response context -> ATC
+        accessTokenContextLookupStrategy = new ChildContextLookup<>(AccessTokenContext.class, true).compose(
+                new ChildContextLookup<>(OIDCAuthenticationResponseContext.class).compose(
+                        new InboundMessageContextLookup()));
+    }
+
+    /**
+     * 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);
+        
+        accessTokenContextLookupStrategy =
+                Constraint.isNotNull(strategy, "AccessTokenContext creation strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        tokenCtx = accessTokenContextLookupStrategy.apply(profileRequestContext);
+        if (tokenCtx == null) {
+            log.error("{} AccessTokenContext is missing", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        
+        if (tokenCtx.getJWT() == null) {
+            log.debug("{} JWT is absent, nothing to do", getLogPrefix());
+            return false;
+        }
+        
+        try {
+            claimsSet = tokenCtx.getJWT().getJWTClaimsSet();
+        } catch (final ParseException e) {
+            log.error("{} Access token JWT parsing failed: {}", getLogPrefix(), e.getMessage());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
+            return false;
+        }
+        
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull protected JWTClaimsSet getClaimsSetToSign() {
+        return claimsSet;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void setSignedJWT(@Nonnull final SignedJWT jwt) {
+        tokenCtx.setJWT(jwt);
+    }
+
+}
\ 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 59585a94..5b325fb3 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
@@ -63,6 +63,8 @@ import com.nimbusds.oauth2.sdk.ResponseType;
 import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
 import com.nimbusds.openid.connect.sdk.OIDCResponseTypeValue;
 import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.UserInfo;
 
 /**
  * Action that adds claims to a {@link ClaimsSet}. Claims are formed of resolved attributes having OIDC encoder. Action
@@ -110,9 +112,6 @@ public class AddAttributesToClaimsSet extends AbstractOIDCResponseAction {
     /** Claims Set to use. */
     @Nullable private ClaimsSet claimsSet;
 
-    /** Whether we are adding claims to ID Token. */
-    private boolean targetIDToken;
-
     /** Whether we can add claims to IDToken by default i.e. response type is "id_token". */
     private boolean addToIDTokenByDefault;
 
@@ -174,16 +173,6 @@ public class AddAttributesToClaimsSet extends AbstractOIDCResponseAction {
         reservedClaimNames = claimNames;
     }
 
-    /**
-     * Set whether target is id token claims set. If this flag is set addToIDTokenByDefault flag is active.
-     * 
-     * @param flag whether target is id token claims set
-     */
-    public void setTargetIDToken(final boolean flag) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        targetIDToken = flag;
-    }
-
     /**
      * Set the strategy used to locate the response {@link ClaimsSet} associated with a given
      * {@link ProfileRequestContext}.
@@ -282,7 +271,7 @@ public class AddAttributesToClaimsSet extends AbstractOIDCResponseAction {
             return false;
         }
         
-        if (targetIDToken) {
+        if (claimsSet instanceof IDTokenClaimsSet) {
             final Object msg = profileRequestContext.getInboundMessageContext().getMessage();
             if (msg instanceof AuthenticationRequest) {
                 final ResponseType responseType = ((AuthenticationRequest) msg).getResponseType();
@@ -295,7 +284,7 @@ public class AddAttributesToClaimsSet extends AbstractOIDCResponseAction {
                 alwaysIncludedAttributes = Collections.emptySet();
             }
             deniedUserInfoAttributes = Collections.emptySet();
-        } else {
+        } else if (claimsSet instanceof UserInfo) {
             deniedUserInfoAttributes = deniedUserInfoAttributesLookupStrategy.apply(profileRequestContext);
             if (deniedUserInfoAttributes == null) {
                 deniedUserInfoAttributes = Collections.emptySet();
@@ -305,8 +294,7 @@ public class AddAttributesToClaimsSet extends AbstractOIDCResponseAction {
         return true;
     }
 
-    // Checkstyle: CyclomaticComplexity OFF
-
+// Checkstyle: CyclomaticComplexity OFF
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -346,7 +334,7 @@ public class AddAttributesToClaimsSet extends AbstractOIDCResponseAction {
         for (final JSONObject claim : claims) {
             for (final String name : claim.keySet()) {
                 if (reservedClaimNames != null && reservedClaimNames.contains(name)) {
-                    log.debug("{} claim has a reserved name {}. Not added to claims set", getLogPrefix(), name);
+                    log.debug("{} Claim has a reserved name ({}), not added to claims set", getLogPrefix(), name);
                     continue;
                 }
                 
@@ -355,7 +343,7 @@ public class AddAttributesToClaimsSet extends AbstractOIDCResponseAction {
             }
         }
         
-        log.debug("{} claims set after mapping attributes to claims {}", getLogPrefix(),
+        log.debug("{} Claims set after mapping attributes to claims {}", getLogPrefix(),
                 claimsSet.toJSONObject().toJSONString());
     }
 
@@ -383,12 +371,12 @@ public class AddAttributesToClaimsSet extends AbstractOIDCResponseAction {
         
         for (final TranscodingRule rule : transcodingRules) {
             try {
-                if (targetIDToken) {
-                    if (!addToIDTokenByDefault && !alwaysIncludedAttributes.contains(attribute.getId())) {
-                        log.debug("{} Attribute {} not targeted for ID Token", getLogPrefix(), attribute.getId());
-                        continue;
-                    }
-                } else if (deniedUserInfoAttributes.contains(attribute.getId())) {
+                // Check for claims to skip based on token type.
+                if (claimsSet instanceof IDTokenClaimsSet && !addToIDTokenByDefault
+                        && !alwaysIncludedAttributes.contains(attribute.getId())) {
+                    log.debug("{} Attribute {} not targeted for ID Token", getLogPrefix(), attribute.getId());
+                    continue;
+                } else if (claimsSet instanceof UserInfo && deniedUserInfoAttributes.contains(attribute.getId())) {
                     log.debug("{} Attribute {} not targeted for Userinfo Token", getLogPrefix(), attribute.getId());
                     continue;
                 }
@@ -407,7 +395,6 @@ public class AddAttributesToClaimsSet extends AbstractOIDCResponseAction {
             }
         }
     }
-
-    // Checkstyle: CyclomaticComplexity ON
+// Checkstyle: CyclomaticComplexity ON
 
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContext.java
index 672365ad..c4d143fd 100644
--- 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
@@ -144,7 +144,7 @@ public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction
     }
 
     /**
-     * Set the strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext} associated with a given
+     * Set the strategy used to locate the {@link OIDCAuthenticationResponseConsentContext} associated with a given
      * {@link ProfileRequestContext}.
      * 
      * @param strategy lookup strategy
@@ -292,7 +292,7 @@ public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction
                     .setScope(getOidcResponseContext().getScope())
                     .setACR(getOidcResponseContext().getAcr())
                     .setNonce(authenticationRequest.getNonce())
-                    .setClaims(authenticationRequest.getOIDCClaims())
+                    .setClaimsRequest(authenticationRequest.getOIDCClaims())
                     .setDlClaims(claims)
                     .setDlClaimsUI(claimsUI)
                     .setConsentedClaims(consented)
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 51a73a33..34cab869 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
@@ -117,7 +117,9 @@
                 <value>sub</value>
                 <value>iat</value>
                 <value>exp</value>
+                <value>nbf</value>
                 <value>acr</value>
+                <value>amr</value>
                 <value>auth_time</value>
                 <value>at_hash</value>
                 <value>c_hash</value>
@@ -125,6 +127,25 @@
             </list>
         </property>
     </bean>
+
+    <bean id="shibboleth.oidc.DefaultAccessTokenReservedClaimNames" lazy-init="true"
+            class="org.springframework.beans.factory.config.ListFactoryBean">
+        <property name="sourceList">
+            <list>
+                <value>jti</value>
+                <value>aud</value>
+                <value>iss</value>
+                <value>prn</value>
+                <value>sub</value>
+                <value>iat</value>
+                <value>exp</value>
+                <value>nbf</value>
+                <value>acr</value>
+                <value>amr</value>
+                <value>auth_time</value>
+            </list>
+        </property>
+    </bean>
     
     <bean id="shibboleth.oidc.DefaultUserInfoReservedClaimNames" lazy-init="true"
             class="org.springframework.beans.factory.config.ListFactoryBean">
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
index 7101c04a..09510db0 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
@@ -40,6 +40,7 @@
     <bean id="ResolveAttributes" class="net.shibboleth.idp.profile.impl.ResolveAttributes" scope="prototype"
         c:resolverService-ref="shibboleth.AttributeResolverService"
         p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
+        p:resolutionContextDecorator="#{getObject('OAuthResolutionContextDecorator')}"
         p:maskFailures="%{idp.service.attribute.resolver.maskFailures:true}" />
 
     <bean id="FilterAttributes" class="net.shibboleth.idp.profile.impl.FilterAttributes" scope="prototype"
@@ -237,6 +238,18 @@
                     <bean class="net.shibboleth.idp.plugin.oidc.op.audit.impl.SubjectTypeAuditExtractor">
                     </bean>
                 </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.plugin.oidc.op.audit.AuditFields.AUDIENCE"/>
+                    </key>
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ValidatedAudienceLookupFunction" />
+                </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.plugin.oidc.op.audit.AuditFields.SCOPE"/>
+                    </key>
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ValidatedAudienceLookupFunction" />
+                </entry>
                 <entry>
                     <key>
                         <util:constant static-field="net.shibboleth.idp.plugin.oidc.op.audit.AuditFields.ACR"/>
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 db8a5960..0785ebe9 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
@@ -275,7 +275,6 @@
 
     <bean id="AddAttributeClaimsToIDToken"
             class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAttributesToClaimsSet" scope="prototype"
-            p:targetIDToken="true"
             p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
             p:reservedClaimNames="#{getObject('shibboleth.oidc.IDTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultIDTokenReservedClaimNames')}">
         <property name="activationCondition">
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 15f446c9..d5d44758 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
@@ -24,6 +24,9 @@
         class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestClientIDLookupFunction" />
 
     <bean id="ResolveAttributesPredicate" class="net.shibboleth.oidc.profile.config.logic.ResolveAttributesPredicate" />
+    
+    <bean id="OAuthResolutionContextDecorator"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.OAuthAttributeResolutionContextDecorator" />
 
     <bean id="InitializeOutboundMessageContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeOutboundTokenResponseMessageContext"
@@ -32,10 +35,32 @@
     <bean id="ValidateGrantType" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateGrantType"
         scope="prototype" />
 
-    <bean id="ValidateGrant" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateGrant" scope="prototype"
-        c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
-        p:replayCache-ref="shibboleth.ReplayCache"
-        p:revocationCache-ref="shibboleth.RevocationCache" />
+    <bean id="PopulateTokenEndpointJwtSignatureValidationParameters"
+            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters "
+            scope="prototype"
+            p:configurationLookupStrategy-ref="shibboleth.oidc.SignatureValidationConfigurationLookup"
+            p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenEndpointJwtSignatureValidationParametersResolver">
+        <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="shibboleth.oidc.SignatureValidationConfigurationLookup"
+        class="net.shibboleth.oidc.profile.config.navigate.TokenEndpointJwtSignatureValidationConfigurationLookupFunction"
+        p:relyingPartyConfigurationResolver-ref="shibboleth.RelyingPartyConfigurationResolver" />
+
+    <bean id="shibboleth.oidc.TokenEndpointJwtSignatureValidationParametersResolver"
+        class="net.shibboleth.idp.plugin.oidc.op.security.impl.OIDCClientInformationSignatureValidationParametersResolver"
+        p:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache"
+        p:keyFetchInterval="%{idp.oidc.jwksuri.fetchInterval:PT30M}"
+        p:parameterType="#{T(net.shibboleth.idp.plugin.oidc.op.security.impl.OIDCClientInformationSignatureValidationParametersResolver.ParameterType).TOKEN_ENDPOINT_JWT_VALIDATION}" />
 
     <!-- Condition signaling that request was NOT for client_credentials grant. -->        
     <bean id="NotClientCredentialsGrantCondition" parent="shibboleth.Conditions.NOT">
@@ -49,6 +74,13 @@
     <bean id="AuthorizationCodeGrantCondition"
         class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.RequestedGrantTypesCondition"
         p:grantTypes="T(com.nimbusds.oauth2.sdk.GrantType).AUTHORIZATION_CODE" />
+
+    <!-- Traditional third-party grant handling. -->
+
+    <bean id="ValidateGrant" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateGrant" scope="prototype"
+        c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
+        p:replayCache-ref="shibboleth.ReplayCache"
+        p:revocationCache-ref="shibboleth.RevocationCache" />
     
     <bean id="ValidatePKCE" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidatePKCE" scope="prototype" />
 
@@ -76,7 +108,7 @@
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationContextClassReferenceFromAuthzCodeToResponseContext"
         scope="prototype" />
 
-    <bean id="SetAuthenticationTimeToResponseContext"
+    <bean id="SetAuthenticationTimeFromAuthzCodeToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationTimeToResponseContext" scope="prototype"
         p:authTimeLookupStrategy-ref="shibboleth.TokenRequestAuthTimeLookupFunction" />
 
@@ -93,17 +125,33 @@
     <bean id="InitializeSubjectContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeSubjectContext" scope="prototype" />
 
-    <bean id="SetSubjectToResponseContext"
+    <bean id="SetSubjectFromAuthzCodeToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSubjectToResponseContext" scope="prototype" />
 
+    <!-- client_credentials grant handling. -->
+
+    <bean id="SetAuthenticationContextClassReferenceToResponseContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationContextClassReferenceToResponseContext"
+        scope="prototype" />
+
+    <bean id="SetAuthenticationTimeToResponseContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationTimeToResponseContext" scope="prototype" />
+
+    <!-- Common grant handling.  -->
+
     <bean id="ValidateScope" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateScope" scope="prototype"
         p:requestedScopesLookupStrategy-ref="shibboleth.TokenRequestScopeLookupStrategy" />
 
+    <bean id="shibboleth.TokenRequestScopeLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestScopeLookupFunction" />
+
     <!--
     TODO: May need to eventually conditionalize this and the encryption action following.
     With opaque access tokens, this wouldn't be used for the client_credentials grant, but
     with JWT access tokens it will be used for ID and access tokens. For now leaving enabled.
+    Encryption has to be redone anyway since the recipient isn't the client but the resource server.
     -->
+
     <bean id="PopulateTokenSignatureSigningParameters"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters " scope="prototype"
         p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
@@ -130,10 +178,9 @@
         p:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache"
         p:keyFetchInterval="%{idp.oidc.jwksuri.fetchInterval:PT30M}" />
 
-    <bean id="shibboleth.TokenRequestScopeLookupStrategy"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestScopeLookupFunction" />
+    <!-- Traditional third-party grant response handling. -->
 
-    <bean id="SetAccessTokenToResponseContext"
+    <bean id="SetOIDCAccessTokenToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAccessTokenToResponseContext" scope="prototype"
         c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}" />
 
@@ -146,7 +193,6 @@
 
     <bean id="AddAttributeClaimsToIDToken"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAttributesToClaimsSet" scope="prototype"
-        p:targetIDToken="true"
         p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
         p:reservedClaimNames="#{getObject('shibboleth.oidc.IDTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultIDTokenReservedClaimNames')}" />
 
@@ -163,6 +209,10 @@
     <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" />
+
     <bean id="AddAccessTokenHashToIDToken"
             class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAccessTokenHashToIDToken" scope="prototype">
         <property name="securityParametersLookupStrategy">
@@ -172,9 +222,6 @@
         </property>
     </bean>
 
-    <bean id="shibboleth.TokenRequestNonceLookupStrategy"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestNonceLookupFunction" scope="prototype" />
-
     <bean id="SignIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SignIDToken" scope="prototype">
         <property name="securityParametersLookupStrategy">
             <bean parent="shibboleth.Functions.Compose"
@@ -198,30 +245,32 @@
         </property>
     </bean>
 
-    <bean id="PopulateTokenEndpointJwtSignatureValidationParameters"
-            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters " scope="prototype"
-            p:configurationLookupStrategy-ref="shibboleth.oidc.SignatureValidationConfigurationLookup"
-            p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenEndpointJwtSignatureValidationParametersResolver">
-        <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>
+    <!-- client_credentials grant response actions. -->
 
-    <bean id="shibboleth.oidc.SignatureValidationConfigurationLookup"
-        class="net.shibboleth.oidc.profile.config.navigate.TokenEndpointJwtSignatureValidationConfigurationLookupFunction"
-        p:relyingPartyConfigurationResolver-ref="shibboleth.RelyingPartyConfigurationResolver" />
+    <bean id="AddAttributeClaimsToAccessToken"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAttributesToClaimsSet" scope="prototype"
+        p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
+        p:responseClaimsSetLookupStrategy-ref="shibboleth.AccessTokenClaimsSetLookupStrategy"
+        p:reservedClaimNames="#{getObject('shibboleth.oidc.AccessTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultAccessTokenReservedClaimNames')}" />
+
+    <bean id="shibboleth.AccessTokenClaimsSetLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.AccessTokenClaimsSetLookupFunction" />
+
+    <bean id="BuildAccessToken"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.BuildAccessToken" scope="prototype"
+        p:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
+        p:useUnfilteredAttributes="%{idp.oauth.accessToken.useUnfilteredAttributes:true}"
+        p:scopeAttribute="#{'%{idp.oauth.accessToken.scopeAttribute:scope}'.trim()}"
+        p:audienceAttribute="#{'%{idp.oauth.accessToken.audienceAttribute:audience}'.trim()}" />
+        
+    <bean id="SignAccessToken"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SignAccessToken" scope="prototype" />
+
+    <bean id="SetOAuthAccessTokenToResponseContext"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SetAccessTokenToResponseContext"
+        scope="prototype" />
 
-    <bean id="shibboleth.oidc.TokenEndpointJwtSignatureValidationParametersResolver"
-        class="net.shibboleth.idp.plugin.oidc.op.security.impl.OIDCClientInformationSignatureValidationParametersResolver"
-        p:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache" p:keyFetchInterval="%{idp.oidc.jwksuri.fetchInterval:PT30M}"
-        p:parameterType="#{T(net.shibboleth.idp.plugin.oidc.op.security.impl.OIDCClientInformationSignatureValidationParametersResolver.ParameterType).TOKEN_ENDPOINT_JWT_VALIDATION}" />
+    <!-- Audit extractors. -->
 
     <bean id="shibboleth.oidc.DefaultPostResponseAuditExtractorsForFlow"
             parent="shibboleth.oidc.DefaultPostResponseAuditExtractors"
@@ -230,8 +279,7 @@
             <map merge="true">
                 <entry>
                     <key>
-                        <util:constant
-                            static-field="net.shibboleth.idp.plugin.oidc.op.audit.AuditFields.NONCE"/>
+                        <util:constant static-field="net.shibboleth.idp.plugin.oidc.op.audit.AuditFields.NONCE"/>
                     </key>
                     <bean class="net.shibboleth.idp.plugin.oidc.op.audit.impl.IdTokenClaimsAuditExtractor"
                         c:claim="nonce" />
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 e8888e9e..5bbb78f1 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
@@ -39,7 +39,7 @@
     <decision-state id="BranchOnGrantType">
         <if test="NotClientCredentialsGrantCondition.test(opensamlProfileRequestContext)"
             then="TraditionalGrantProcessing"
-            else="CommonProcessing" />
+            else="ClientCredentialsGrantProcessing" />
     </decision-state>
 
     <!-- These steps apply to grants that rely on the authorization endpoint to "prime" the token request. -->
@@ -49,11 +49,19 @@
         <evaluate expression="ValidateRedirectURI" />
         <evaluate expression="SetRequestedClaimsToResponseContext" />
         <evaluate expression="SetAuthenticationContextClassReferenceFromAuthzCodeToResponseContext" />
-        <evaluate expression="SetAuthenticationTimeToResponseContext" />
+        <evaluate expression="SetAuthenticationTimeFromAuthzCodeToResponseContext" />
         <evaluate expression="SetTokenDeliveryAttributesFromTokenToResponseContext" />
         <evaluate expression="SetConsentToResponseContext" />
         <evaluate expression="InitializeSubjectContext" />
-        <evaluate expression="SetSubjectToResponseContext" />
+        <evaluate expression="SetSubjectFromAuthzCodeToResponseContext" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="CommonGrantProcessing" />
+    </action-state>
+
+    <!-- These steps apply to grants that are self-contained on this endpoint. -->
+    <action-state id="ClientCredentialsGrantProcessing">
+        <evaluate expression="SetAuthenticationContextClassReferenceToResponseContext" />
+        <evaluate expression="SetAuthenticationTimeToResponseContext" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="CommonGrantProcessing" />
     </action-state>
@@ -82,8 +90,14 @@
     
     <!-- Consent check is not applicable to some grant types but does no harm. -->
     
-    <action-state id="BuildResponse">
-        <evaluate expression="SetAccessTokenToResponseContext" />
+    <decision-state id="BuildResponse">
+        <if test="NotClientCredentialsGrantCondition.test(opensamlProfileRequestContext)"
+            then="TraditionalGrantResponse"
+            else="ClientCredentialsGrantResponse" />
+    </decision-state>
+    
+    <action-state id="TraditionalGrantResponse">
+        <evaluate expression="SetOIDCAccessTokenToResponseContext" />
         <evaluate expression="SetRefreshTokenToResponseContext" />
         <evaluate expression="AddIDTokenShell" />
         <evaluate expression="AddAttributeClaimsToIDToken" />
@@ -98,6 +112,15 @@
         <transition on="proceed" to="BuildResponseMessage" />
     </action-state>
 
+    <action-state id="ClientCredentialsGrantResponse">
+        <evaluate expression="AddAttributeClaimsToAccessToken" />
+        <evaluate expression="BuildAccessToken" />
+        <evaluate expression="SignAccessToken" />
+        <evaluate expression="SetOAuthAccessTokenToResponseContext" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="BuildResponseMessage" />
+    </action-state>
+
     <bean-import resource="token-beans.xml" />
 
 </flow>
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 3441930a..e9c95406 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
@@ -27,12 +27,10 @@
         p:revocationCache-ref="shibboleth.RevocationCache" />
 
     <bean id="shibboleth.ClientIDLookupStrategy"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.UserInfoRequestClientIDLookupFunction"
-        scope="prototype" />
+        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"
-        scope="prototype" />
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.UserInfoRequestClientIDLookupFunction" />
 
     <bean id="ValidateScope" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateScope" scope="prototype">
         <property name="requestedScopesLookupStrategy">
@@ -88,8 +86,7 @@
         p:parameterType="#{T(net.shibboleth.idp.plugin.oidc.op.security.impl.OIDCClientInformationEncryptionParametersResolver.ParameterType).USERINFO_ENCRYPTION}" />
 
     <bean id="shibboleth.TokenRequestRequestedClaimsLookupFunction"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestRequestedClaimsLookupFunction"
-        scope="prototype" />
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestRequestedClaimsLookupFunction" />
 
     <bean id="InitializeSubjectContext" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeSubjectContext"
         scope="prototype" />
@@ -111,8 +108,7 @@
         p:responseClaimsSetLookupStrategy-ref="shibboleth.UserInfoResponseClaimsSetLookupStrategy" scope="prototype" />
 
     <bean id="shibboleth.UserInfoResponseClaimsSetLookupStrategy"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.UserInfoResponseClaimsSetLookupFunction"
-        scope="prototype" />
+        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">
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSetTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSetTest.java
index 633311cf..02fc13da 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSetTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSetTest.java
@@ -108,7 +108,6 @@ public class AddAttributesToClaimsSetTest extends BaseOIDCResponseActionTest {
         registry.initialize();
 
         action = new AddAttributesToClaimsSet();
-        action.setTargetIDToken(true);
         action.setTranscoderRegistry(new MockReloadableService<>(registry));
         action.initialize();
     }

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


More information about the commits mailing list