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

Scott Cantor cantor.2 at osu.edu
Tue Jan 18 22:30:10 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=ec4e14b548fcc9b872366cbf32e21f205fce7980

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

commit ec4e14b548fcc9b872366cbf32e21f205fce7980
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Jan 18 17:30:06 2022 -0500

    JOIDC-11 - Support for client_credentials grant
    
    https://shibboleth.atlassian.net/browse/JOIDC-11
    
    Test and fixes to BuildAccessToken.
    Adjust handling of various token fields.
    Expose more standard claims on TokenClaimsSet.
    Fix naming of client_id claim with backward compatibility.
---
 .../op/token/support/AccessTokenClaimsSet.java     |   2 +-
 .../oidc/op/token/support/TokenClaimsSet.java      |  96 ++++--
 .../op/oauth2/profile/impl/BuildAccessToken.java   |  65 ++--
 .../idp/flows/oidc/token/token-beans.xml           |  14 +
 .../shibboleth/idp/flows/oidc/token/token-flow.xml |   1 +
 .../oauth2/profile/impl/BuildAccessTokenTest.java  | 378 +++++++++++++++++++++
 .../op/oauth2/profile/impl/RevokeTokenTest.java    |   1 -
 .../profile/impl/BaseOIDCResponseActionTest.java   |  10 +-
 8 files changed, 514 insertions(+), 53 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
index aa0b098a..e3ac61c4 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
@@ -189,7 +189,7 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
             setAuthenticationTime(existing.getAuthenticationTime());
             setAudience(existing.getAudience());
             setRedirectURI(existing.getRedirectURI());
-            setClaims(existing.getClaimsRequest());
+            setClaimsRequest(existing.getClaimsRequest());
             setConsentedClaims(existing.getConsentedClaims());
             setConsentEnabled(existing.isConsentEnabled());
         }
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 ea9b6e5e..6ee9af7a 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
@@ -77,7 +77,10 @@ public class TokenClaimsSet {
     @Nonnull @NotEmpty public static final String KEY_SUBJECT = "sub";
 
     /** Client id of the rp the token is generated for. */
-    @Nonnull @NotEmpty public static final String KEY_CLIENTID = "clid";
+    @Nonnull @NotEmpty public static final String KEY_CLIENTID = "client_id";
+
+    /** Client id of the rp the token is generated for (old constant). */
+    @Nonnull @NotEmpty public static final String KEY_LEGACY_CLIENTID = "clid";
 
     /** Expiration time of the token. */
     @Nonnull @NotEmpty public static final String KEY_EXPIRATION_TIME = "exp";
@@ -177,8 +180,9 @@ public class TokenClaimsSet {
         if (tokenClaimsSet.getStringClaim(KEY_SUBJECT) == null) {
             throw new ParseException("claim sub must exist and not be null", 0);
         }
-        if (tokenClaimsSet.getStringClaim(KEY_CLIENTID) == null) {
-            throw new ParseException("claim clid must exist and not be null", 0);
+        if (tokenClaimsSet.getStringClaim(KEY_CLIENTID) == null
+                && tokenClaimsSet.getStringClaim(KEY_LEGACY_CLIENTID) == null) {
+            throw new ParseException("claim client_id (or clid) must exist and not be null", 0);
         }
         if (tokenClaimsSet.getDateClaim(KEY_EXPIRATION_TIME) == null) {
             throw new ParseException("claim exp must exist and not be null", 0);
@@ -265,20 +269,29 @@ public class TokenClaimsSet {
     @Nullable public JWTClaimsSet getClaimsSet() {
         return tokenClaimsSet;
     }
-
+    
     /**
-     * Check if the token is expired.
+     * Get the issuer.
      * 
-     * <p>Replaced by {@link #isTimeValid} method that enforces both bounds.</p>
+     * @return issuer
      * 
-     * @return true if the token is expired, otherwise false
+     * @since 3.1.0
+     */
+    @Nonnull @NotEmpty public String getIssuer() {
+        Constraint.isNotNull(tokenClaimsSet, "JWTClaimsSet cannot be null");
+        return tokenClaimsSet.getIssuer();
+    }
+    
+    /**
+     * Get issuance time of the token.
      * 
-     * @deprecated
+     * @return issuance time
+     * 
+     * @since 3.1.0
      */
-    @Deprecated(since="3.1.0", forRemoval=true)
-    public boolean isExpired() {
+    @Nonnull public Instant getIssuedAt() {
         Constraint.isNotNull(tokenClaimsSet, "JWTClaimsSet cannot be null");
-        return tokenClaimsSet.getExpirationTime().before(new Date());
+        return tokenClaimsSet.getIssueTime().toInstant();
     }
 
     /**
@@ -290,6 +303,34 @@ public class TokenClaimsSet {
         Constraint.isNotNull(tokenClaimsSet, "JWTClaimsSet cannot be null");
         return tokenClaimsSet.getExpirationTime().toInstant();
     }
+    
+    /**
+     * Get not before time of the token, if any.
+     * 
+     * @return not before time of the token
+     * 
+     * @since 3.1.0
+     */
+    @Nullable public Instant getNotBefore() {
+        Constraint.isNotNull(tokenClaimsSet, "JWTClaimsSet cannot be null");
+        final Date d = tokenClaimsSet.getNotBeforeTime();
+        return d != null ? d.toInstant() : null;
+    }
+
+    /**
+     * Check if the token is expired.
+     * 
+     * <p>Replaced by {@link #isTimeValid} method that enforces both bounds.</p>
+     * 
+     * @return true if the token is expired, otherwise false
+     * 
+     * @deprecated
+     */
+    @Deprecated(since="3.1.0", forRemoval=true)
+    public boolean isExpired() {
+        Constraint.isNotNull(tokenClaimsSet, "JWTClaimsSet cannot be null");
+        return tokenClaimsSet.getExpirationTime().before(new Date());
+    }
 
     /**
      * Check if the token is valid with respect to expiration and not before limits.
@@ -308,19 +349,6 @@ public class TokenClaimsSet {
         return false;
     }
     
-    /**
-     * Get not before time of the token, if any.
-     * 
-     * @return not before time of the token
-     * 
-     * @since 3.1.0
-     */
-    @Nullable public Instant getNotBefore() {
-        Constraint.isNotNull(tokenClaimsSet, "JWTClaimsSet cannot be null");
-        final Date d = tokenClaimsSet.getNotBeforeTime();
-        return d != null ? d.toInstant() : null;
-    }
-    
     /**
      * Get redirect uri of the request.
      * 
@@ -366,6 +394,18 @@ public class TokenClaimsSet {
         Constraint.isNotNull(tokenClaimsSet, "JWTClaimsSet cannot be null");
         return (String) tokenClaimsSet.getClaim(KEY_USER_PRINCIPAL);
     }
+    
+    /**
+     * Get subject claim.
+     * 
+     * @return subject claim
+     * 
+     * @since 3.1.0
+     */
+    @Nullable public String getSubject() {
+        Constraint.isNotNull(tokenClaimsSet, "JWTClaimsSet cannot be null");
+        return (String) tokenClaimsSet.getClaim(KEY_SUBJECT);
+    }
 
     /**
      * Get auth time of the user.
@@ -570,7 +610,11 @@ public class TokenClaimsSet {
      */
     @Nullable public ClientID getClientID() {
         Constraint.isNotNull(tokenClaimsSet, "JWTClaimsSet cannot be null");
-        final Object id = tokenClaimsSet.getClaim(KEY_CLIENTID);
+        Object id = tokenClaimsSet.getClaim(KEY_CLIENTID);
+        if (id == null) {
+            id = tokenClaimsSet.getClaim(KEY_LEGACY_CLIENTID);
+        }
+        
         if (id instanceof String) {
             return new ClientID((String) id);
         }
@@ -692,7 +736,7 @@ public class TokenClaimsSet {
                     .audience(audience)
                     .claim(KEY_NONCE, nonce == null ? null : nonce.getValue())
                     .claim(KEY_AUTH_TIME, Date.from(authTime))
-                    .claim(KEY_REDIRECT_URI, redirect.toString())
+                    .claim(KEY_REDIRECT_URI, redirect == null ? null : redirect.toString())
                     .claim(KEY_SCOPE, reqScope.toString())
                     .claim(KEY_CLAIMS, reqClaims == null ? null : reqClaims.toJSONObject())
                     .claim(KEY_DELIVERY_CLAIMS, dlClaims == null ? null : dlClaims.toJSONObject())
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
index fa170cb6..d4a60429 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
@@ -30,19 +30,21 @@ 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.opensaml.profile.context.navigate.OutboundMessageContextLookup;
 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 com.nimbusds.oauth2.sdk.id.ClientID;
 
 import net.shibboleth.idp.attribute.IdPAttribute;
 import net.shibboleth.idp.attribute.StringAttributeValue;
 import net.shibboleth.idp.attribute.context.AttributeContext;
-import net.shibboleth.idp.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.context.navigate.TokenRequestClientIDLookupFunction;
 import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet.Builder;
@@ -79,6 +81,7 @@ import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifie
  * @event {@link EventIds#MESSAGE_PROC_ERROR}
  * @event {@link EventIds#INVALID_MSG_CTX}
  * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#MESSAGE_PROC_ERROR}
  * @event {@link IdPEventIds#INVALID_ATTRIBUTE_CTX}
  * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
  * @event {@link IdPEventIds#INVALID_SUBJECT_CTX}
@@ -95,6 +98,9 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
     
     /** Strategy used to obtain the response issuer value. */
     @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
+    
+    /** Strategy used to obtain the original client ID. */
+    @Nonnull private Function<ProfileRequestContext,ClientID> clientIDLookupStrategy;
 
     /** Strategy used to obtain the access token type to issue. */
     @Nonnull private Function<ProfileRequestContext,String> accessTokenTypeLookupStrategy;    
@@ -129,9 +135,6 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
     /** Attribute context. */
     @Nullable private AttributeContext attributeCtx;
 
-    /** Subject context. */
-    @Nullable private SubjectContext subjectCtx;
-
     /** The generator to use. */
     @Nullable private IdentifierGenerationStrategy idGenerator;
     
@@ -143,12 +146,16 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         accessTokenTypeLookupStrategy = new AccessTokenTypeLookupFunction();
         accessTokenLifetimeLookupStrategy = new AccessTokenLifetimeLookupFunction();
         issuerLookupStrategy = new ResponderIdLookupFunction();
+        
+        clientIDLookupStrategy = FunctionSupport.compose(new TokenRequestClientIDLookupFunction(),
+                new InboundMessageContextLookup());
+        
         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()));
+                        new OutboundMessageContextLookup()));
         
         // PRC -> RPC -> AC
         attributeContextLookupStrategy = new ChildContextLookup<>(AttributeContext.class).compose(
@@ -200,7 +207,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
      * @param strategy lookup strategy
      */
     public void setIdentifierGeneratorLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, IdentifierGenerationStrategy> strategy) {
+            @Nonnull final Function<ProfileRequestContext,IdentifierGenerationStrategy> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
 
         idGeneratorLookupStrategy =
@@ -227,7 +234,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
      * @param strategy lookup strategy
      */
     public void setAttributeContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, AttributeContext> strategy) {
+            @Nonnull final Function<ProfileRequestContext,AttributeContext> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
 
         attributeContextLookupStrategy =
@@ -239,12 +246,23 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
      * 
      * @param strategy lookup strategy
      */
-    public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+    public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
         issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
     }
     
+    /**
+     * Set the strategy used to locate the original {@link ClientID} from the request.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setClientIDLookupStrategy(@Nonnull final Function<ProfileRequestContext,ClientID> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        clientIDLookupStrategy = Constraint.isNotNull(strategy, "ClientID lookup strategy cannot be null");
+    }
+    
     /**
      * Set whether to source the scope and audience claims from unfiltered attributes.
      * 
@@ -285,7 +303,6 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         audienceAttribute = StringSupport.trimOrNull(id);
     }
 
-// Checkstyle: CyclomaticComplexity OFF
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -305,13 +322,6 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         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());
@@ -343,12 +353,19 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         
         return true;
     }
-// Checkstyle: CyclomaticComplexity ON
 
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
+        final String issuer = issuerLookupStrategy.apply(profileRequestContext);
+        final ClientID clientID = clientIDLookupStrategy.apply(profileRequestContext);
+        if (issuer == null || clientID == null) {
+            log.error("{} Unable to determine issuer or clientID, failing request", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
+            return;
+        }
+        
         final OIDCAuthenticationResponseContext ctx = getOidcResponseContext();
 
         final Collection<String> audience = getAudience();
@@ -363,14 +380,15 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         final Scope scope = getScope(ctx.getScope());
         log.debug("{} Building access token with scope: {}", getLogPrefix(), scope);
 
-        final Instant dateExp = Instant.now().plus(accessTokenCtx.getLifetime());
+        final Instant now = Instant.now();
+        final Instant dateExp = 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())
+                .setClientID(clientID)
+                .setIssuer(issuer)
+                .setSubject(ctx.getSubject())
+                .setIssuedAt(now)
                 .setExpiresAt(dateExp)
                 .setACR(ctx.getAcr())
                 .setAuthenticationTime(ctx.getAuthTime())
@@ -441,6 +459,9 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         }
         
         log.debug("{} Using originally requested resource(s) as audience", getLogPrefix());
+        if (tokenRequest.getResources() == null) {
+            return null;
+        }
         return tokenRequest.getResources().stream().map(URI::toString).collect(Collectors.toUnmodifiableList());
     }
 
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 d5d44758..984ca005 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
@@ -137,6 +137,20 @@
     <bean id="SetAuthenticationTimeToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationTimeToResponseContext" scope="prototype" />
 
+    <bean id="SetSubjectFromSubjectContextToResponseContext"
+            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSubjectToResponseContext" scope="prototype">
+        <property name="subjectLookupStrategy">
+            <bean parent="shibboleth.Functions.Compose">
+                <constructor-arg>
+                    <bean class="net.shibboleth.idp.authn.context.navigate.SubjectContextPrincipalLookupFunction" />
+                </constructor-arg>
+                <constructor-arg>
+                    <ref bean="shibboleth.ChildLookup.SubjectContext"/>
+                </constructor-arg>
+            </bean>
+        </property>
+    </bean>
+
     <!-- Common grant handling.  -->
 
     <bean id="ValidateScope" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateScope" scope="prototype"
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 5bbb78f1..de879956 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
@@ -62,6 +62,7 @@
     <action-state id="ClientCredentialsGrantProcessing">
         <evaluate expression="SetAuthenticationContextClassReferenceToResponseContext" />
         <evaluate expression="SetAuthenticationTimeToResponseContext" />
+        <evaluate expression="SetSubjectFromSubjectContextToResponseContext" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="CommonGrantProcessing" />
     </action-state>
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
new file mode 100644
index 00000000..f23f63b3
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
@@ -0,0 +1,378 @@
+/*
+ * 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 static org.testng.Assert.*;
+
+import net.shibboleth.idp.attribute.EmptyAttributeValue;
+import net.shibboleth.idp.attribute.EmptyAttributeValue.EmptyType;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.AccessTokenContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+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.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+import net.shibboleth.utilities.java.support.security.DataSealerException;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.EventIds;
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.oauth2.sdk.ClientCredentialsGrant;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+// Checkstyle: ThrowsCount OFF
+
+/** {@link SetAccessTokenToResponseContext} unit test. */
+public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
+
+    /** Action to test. */
+    private BuildAccessToken action;
+    
+    @BeforeMethod
+    protected void setUp() throws Exception {
+        super.setUp();
+        
+        final TokenRequest req = new TokenRequest(new URI("http://localhost"),
+                new ClientSecretBasic(new ClientID(clientId), new Secret("foo")),
+                new ClientCredentialsGrant(),
+                Scope.parse("email profile"),
+                Collections.singletonList(new URI("https://rp.example.org")),
+                null);
+        profileRequestCtx.getInboundMessageContext().setMessage(req);
+        
+        respCtx.setScope(new Scope());
+        respCtx.setAuthTime(Instant.now());
+        respCtx.setSubject(clientId);
+        respCtx.setAcr("0");
+        
+        action = new BuildAccessToken();
+    }
+
+    /**
+     * Test invalid message cases.
+     * 
+     * @throws ComponentInitializationException 
+     * @throws NoSuchAlgorithmException 
+     */
+    @Test
+    public void testBadRequest() throws NoSuchAlgorithmException, ComponentInitializationException {
+        initAction(null, null, null);
+        
+        profileRequestCtx.getInboundMessageContext().setMessage(request);
+        Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MSG_CTX);
+
+        profileRequestCtx.getInboundMessageContext().setMessage(null);
+        event = action.execute(requestCtx);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MSG_CTX);
+    }
+
+    /**
+     * Test no client ID.
+     * 
+     * @throws ComponentInitializationException 
+     * @throws NoSuchAlgorithmException 
+     */
+    @Test
+    public void testNoClientID() throws NoSuchAlgorithmException, ComponentInitializationException {
+        action.setClientIDLookupStrategy(FunctionSupport.constant(null));
+        action.setScopeAttribute(null);
+        action.setAudienceAttribute(null);
+        action.initialize();
+        
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertEvent(event, EventIds.MESSAGE_PROC_ERROR);
+    }
+
+    /**
+     * Test no source of audience claim.
+     * 
+     * @throws ParseException 
+     * @throws DataSealerException 
+     * @throws ComponentInitializationException 
+     * @throws NoSuchAlgorithmException
+     * @throws URISyntaxException 
+     */
+    @Test
+    public void testNoAudience() throws ParseException, DataSealerException, ComponentInitializationException,
+            NoSuchAlgorithmException, URISyntaxException {
+
+        final TokenRequest req = new TokenRequest(new URI("http://localhost"),
+                new ClientSecretBasic(new ClientID(clientId), new Secret("foo")),
+                new ClientCredentialsGrant(),
+                Scope.parse("email profile"));
+        profileRequestCtx.getInboundMessageContext().setMessage(req);
+
+        initAction(null, null, null);
+        
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertEvent(event, EventIds.ACCESS_DENIED);
+    }
+    
+    /**
+     * Basic success case, direct reuse of requested scope/audience.
+     * 
+     * @throws ParseException 
+     * @throws DataSealerException 
+     * @throws ComponentInitializationException 
+     * @throws NoSuchAlgorithmException
+     */
+    @Test
+    public void testOpaqueSuccess() throws ParseException, DataSealerException, ComponentInitializationException,
+            NoSuchAlgorithmException {
+
+        initAction(null, null, null);
+        
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
+                Collections.singletonList("https://rp.example.org"));
+    }
+
+    /**
+     * Basic success case, resolve scope.
+     * 
+     * @throws ParseException 
+     * @throws DataSealerException 
+     * @throws ComponentInitializationException 
+     * @throws NoSuchAlgorithmException
+     */
+    @Test
+    public void testOpaqueSuccessWithScopeResolution() throws ParseException, DataSealerException,
+            ComponentInitializationException, NoSuchAlgorithmException {
+
+        initAction(null, "scope", null);
+        
+        final AttributeContext attributeCtx = rpCtx.getSubcontext(AttributeContext.class, true);
+        final IdPAttribute scope = new IdPAttribute("scope");
+        scope.setValues(List.of(new StringAttributeValue("foo"),
+                new EmptyAttributeValue(EmptyType.NULL_VALUE),
+                new StringAttributeValue("bar")));
+        attributeCtx.setUnfilteredIdPAttributes(List.of(scope));
+        
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), Scope.parse("foo bar"),
+                Collections.singletonList("https://rp.example.org"));
+    }
+
+    /**
+     * Basic success case, resolve audience.
+     * 
+     * @throws ParseException 
+     * @throws DataSealerException 
+     * @throws ComponentInitializationException 
+     * @throws NoSuchAlgorithmException
+     */
+    @Test
+    public void testOpaqueSuccessWithAudResolution() throws ParseException, DataSealerException,
+            ComponentInitializationException, NoSuchAlgorithmException {
+
+        initAction(null, null, "aud");
+        
+        final AttributeContext attributeCtx = rpCtx.getSubcontext(AttributeContext.class, true);
+        final IdPAttribute aud = new IdPAttribute("aud");
+        aud.setValues(List.of(new StringAttributeValue("foo"),
+                new EmptyAttributeValue(EmptyType.NULL_VALUE),
+                new StringAttributeValue("bar")));
+        attributeCtx.setUnfilteredIdPAttributes(List.of(aud));
+        
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
+                List.of("foo", "bar"));
+    }
+    
+    /**
+     * Basic success case, direct reuse of requested scope/audience.
+     * 
+     * @throws ParseException 
+     * @throws ComponentInitializationException 
+     * @throws NoSuchAlgorithmException
+     * @throws DataSealerException 
+     */
+    @Test
+    public void testJWTSuccess() throws ParseException, ComponentInitializationException, NoSuchAlgorithmException,
+            DataSealerException {
+
+        initAction(null, null, null);
+        
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
+                Collections.singletonList("https://rp.example.org"));
+    }
+
+    /**
+     * Basic success case, resolve scope.
+     * 
+     * @throws ParseException 
+     * @throws ComponentInitializationException 
+     * @throws NoSuchAlgorithmException
+     * @throws DataSealerException 
+     */
+    @Test
+    public void testJWTSuccessWithScopeResolution() throws ParseException, ComponentInitializationException,
+            NoSuchAlgorithmException, DataSealerException {
+
+        initAction(null, "scope", null);
+        
+        final AttributeContext attributeCtx = rpCtx.getSubcontext(AttributeContext.class, true);
+        final IdPAttribute scope = new IdPAttribute("scope");
+        scope.setValues(List.of(new StringAttributeValue("foo"),
+                new EmptyAttributeValue(EmptyType.NULL_VALUE),
+                new StringAttributeValue("bar")));
+        attributeCtx.setUnfilteredIdPAttributes(List.of(scope));
+        
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), Scope.parse("foo bar"),
+                Collections.singletonList("https://rp.example.org"));
+    }
+
+    /**
+     * Basic success case, resolve audience.
+     * 
+     * @throws ParseException 
+     * @throws DataSealerException 
+     * @throws ComponentInitializationException 
+     * @throws NoSuchAlgorithmException
+     */
+    @Test
+    public void testJWTSuccessWithAudResolution() throws ParseException, DataSealerException,
+            ComponentInitializationException, NoSuchAlgorithmException {
+
+        initAction("JWT", null, "aud");
+        
+        final AttributeContext attributeCtx = rpCtx.getSubcontext(AttributeContext.class, true);
+        final IdPAttribute aud = new IdPAttribute("aud");
+        aud.setValues(List.of(new StringAttributeValue("foo"),
+                new EmptyAttributeValue(EmptyType.NULL_VALUE),
+                new StringAttributeValue("bar")));
+        attributeCtx.setUnfilteredIdPAttributes(List.of(aud));
+        
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
+                List.of("foo", "bar"));
+    }
+    
+    /**
+     * Init action bean based on test.
+     * 
+     * @param type token type to use
+     * @param scope scope attribute ID
+     * @param audience audience attribute ID
+     * 
+     * @throws ComponentInitializationException
+     * @throws NoSuchAlgorithmException
+     */
+    private void initAction(@Nullable @NotEmpty final String type, @Nullable @NotEmpty final String scope,
+            @Nullable @NotEmpty final String audience)
+                    throws ComponentInitializationException, NoSuchAlgorithmException {
+        if ("JWT".equals(type)) {
+            action.setAccessTokenTypeLookupStrategy(FunctionSupport.constant("JWT"));
+        } else if (type == null) {
+            action.setDataSealer(getDataSealer());
+        }
+
+        action.setScopeAttribute(scope);
+        action.setAudienceAttribute(audience);
+        action.initialize();
+    }
+
+    /**
+     * Verify opaque access token's claims.
+     * 
+     * @param ctx access token context
+     * @param scope scope to check for
+     * @param audiences audiences to check for
+     * 
+     * @throws ComponentInitializationException 
+     * @throws DataSealerException 
+     * @throws ParseException 
+     * @throws NoSuchAlgorithmException 
+     */
+    private void verifyClaims(@Nonnull final AccessTokenContext ctx, @Nonnull final Scope scope,
+            @Nonnull @NonnullElements final Collection<String> audiences)
+            throws NoSuchAlgorithmException, ParseException, DataSealerException, ComponentInitializationException {
+        
+        assertEquals(ctx.getLifetime(), Duration.ofMinutes(10));
+        
+        if (ctx.getOpaque() != null) {
+            final AccessTokenClaimsSet at = AccessTokenClaimsSet.parse(ctx.getOpaque(), getDataSealer());
+            assertNotNull(at);
+            assertEquals(at.getACR(), "0");
+            assertEquals(at.getAudience(), audiences);
+            assertTrue(at.getAuthenticationTime().isBefore(Instant.now()));
+            assertEquals(at.getClientID().getValue(), clientId);
+            assertEquals(at.getExp(), at.getIssuedAt().plusSeconds(600));
+            assertEquals(at.getIssuer(), ActionTestingSupport.OUTBOUND_MSG_ISSUER);
+            assertTrue(at.getIssuedAt().isBefore(Instant.now()));
+            assertEquals(at.getScope(), scope);
+            assertEquals(at.getSubject(), clientId);
+        } else if (ctx.getJWT() != null) {
+            final JWTClaimsSet claims = ctx.getJWT().getJWTClaimsSet();
+            assertNotNull(claims);
+            assertEquals(claims.getStringClaim(TokenClaimsSet.KEY_ACR), "0");
+            assertEquals(claims.getAudience(), audiences);
+            assertTrue(claims.getDateClaim(TokenClaimsSet.KEY_AUTH_TIME).toInstant().isBefore(Instant.now()));
+            assertEquals(claims.getStringClaim(TokenClaimsSet.KEY_CLIENTID), clientId);
+            assertEquals(claims.getExpirationTime().toInstant(), claims.getIssueTime().toInstant().plusSeconds(600));
+            assertEquals(claims.getIssuer(), ActionTestingSupport.OUTBOUND_MSG_ISSUER);
+            assertTrue(claims.getIssueTime().toInstant().isBefore(Instant.now()));
+            assertEquals(claims.getStringClaim(TokenClaimsSet.KEY_SCOPE), scope.toString());
+            assertEquals(claims.getSubject(), clientId);
+        } else {
+            throw new RuntimeException("No token found");
+        }
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/RevokeTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/RevokeTokenTest.java
index 4113146f..db4291c4 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/RevokeTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/RevokeTokenTest.java
@@ -33,7 +33,6 @@ import com.nimbusds.oauth2.sdk.TokenRevocationRequest;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
 
-import net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.RevokeToken;
 import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCache;
 import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java
index 15cb4612..9d4e2109 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java
@@ -80,13 +80,13 @@ public abstract class BaseOIDCResponseActionTest {
 
     protected RelyingPartyContext rpCtx;
 
-    final protected String subject = "generatedSubject";
+    protected final String subject;
 
-    final protected String clientId = "s6BhdRkqt3";
+    protected final String clientId;
 
     private DataSealer dataSealer;
 
-    protected IdentifierGenerationStrategy idGenerator = new SecureRandomIdentifierGenerationStrategy();
+    protected final IdentifierGenerationStrategy idGenerator;
 
     protected ProfileRequestContext profileRequestCtx;
 
@@ -101,6 +101,10 @@ public abstract class BaseOIDCResponseActionTest {
     Credential credentialHMAC;
 
     public BaseOIDCResponseActionTest() {
+        subject = "generatedSubject";
+        clientId = "s6BhdRkqt3";
+        idGenerator = new SecureRandomIdentifierGenerationStrategy();
+        
         try {
             BasicJWKCredentialFactoryBean factory = new BasicJWKCredentialFactoryBean();
             factory.setResource(new ClassPathResource("credentials/idp-signing-es.jwk"));

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


More information about the commits mailing list