[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