[java-idp-oidc] branch main updated: JOIDC-11 - Support for client_credentials grant
Scott Cantor
cantor.2 at osu.edu
Wed Jan 19 22:13: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=7fbdafd93b53ab780f9f93ed5c199fe8fa37e51b
The following commit(s) were added to refs/heads/main by this push:
new 7fbdafd9 JOIDC-11 - Support for client_credentials grant
7fbdafd9 is described below
commit 7fbdafd93b53ab780f9f93ed5c199fe8fa37e51b
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Jan 19 17:13:43 2022 -0500
JOIDC-11 - Support for client_credentials grant
https://shibboleth.atlassian.net/browse/JOIDC-11
First working end to end unit tests with fixes to various bits.
---
.../oidc/op/token/support/TokenClaimsSet.java | 20 +-
.../op/oauth2/profile/impl/BuildAccessToken.java | 38 ++-
.../impl/SetAccessTokenToResponseContext.java | 4 +-
.../op/oauth2/profile/impl/SignAccessToken.java | 4 +-
.../impl/FormOutboundTokenResponseMessage.java | 28 +-
.../oidc/abstract-api/oidc-abstract-api-beans.xml | 9 +-
.../idp/flows/oidc/token/token-beans.xml | 19 +-
.../oauth2/profile/impl/BuildAccessTokenTest.java | 7 +-
.../oidc/op/profile/flow/AbstractOidcFlowTest.java | 6 +-
.../flow/ClientCredentialsTokenFlowTest.java | 353 +++++++++++++++++++++
.../op/profile/flow/IntrospectionFlowTest.java | 2 +-
.../oidc/op/profile/flow/RevocationFlowTest.java | 2 +-
.../plugin/oidc/op/profile/flow/TokenFlowTest.java | 2 +-
.../impl/FormOutboundTokenResponseMessageTest.java | 33 +-
.../src/test/resources/conf/attribute-resolver.xml | 24 ++
.../src/test/resources/conf/oidc.properties | 5 +-
.../src/test/resources/conf/relying-party.xml | 7 +
17 files changed, 496 insertions(+), 67 deletions(-)
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 6ee9af7a..ad7f3f40 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
@@ -634,37 +634,37 @@ public class TokenClaimsSet {
@Nonnull @NotEmpty protected String jwtid;
/** Client Id of the rp. */
- @Nonnull protected ClientID rpId;
+ @Nullable protected ClientID rpId;
/** OP issuer value. */
- @Nonnull @NotEmpty protected String iss;
+ @Nullable @NotEmpty protected String iss;
/** User Principal of the authenticated user. */
- @Nonnull @NotEmpty protected String principal;
+ @Nullable @NotEmpty protected String principal;
/** Subject claim value of the authenticated user. */
- @Nonnull @NotEmpty protected String sub;
+ @Nullable @NotEmpty protected String sub;
/** Authentication context class reference value of the authentication. */
- @Nonnull protected ACR acr;
+ @Nullable protected ACR acr;
/** Issue time of the claims set. */
- @Nonnull protected Instant iat;
+ @Nullable protected Instant iat;
/** Expiration time of the claims set. */
- @Nonnull protected Instant exp;
+ @Nullable protected Instant exp;
/** Not Before time of the claims set. */
@Nullable protected Instant nbt;
/** Authentication time of the user. */
- @Nonnull protected Instant authTime;
+ @Nullable protected Instant authTime;
/** Validated redirect URI of the authentication request. */
- @Nonnull protected URI redirect;
+ @Nullable protected URI redirect;
/** Scope of the token request. */
- @Nonnull protected Scope reqScope;
+ @Nullable protected Scope reqScope;
/** Audience of token request. */
@Nonnull @NonnullElements protected List<String> audience;
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 baca3a62..1a54f307 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
@@ -303,6 +303,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
audienceAttribute = StringSupport.trimOrNull(id);
}
+// Checkstyle: CyclomaticComplexity OFF
/** {@inheritDoc} */
@Override
protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -321,6 +322,11 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
final String tokenType = accessTokenTypeLookupStrategy.apply(profileRequestContext);
jwtTokenType = tokenType != null && "JWT".equals(tokenType);
+ if (!jwtTokenType && dataSealer == null) {
+ log.error("{} DataSealer required for opaque access tokens", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
+ return false;
+ }
idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
if (idGenerator == null) {
@@ -353,6 +359,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
return true;
}
+// Checkstyle: CyclomaticComplexity ON
/** {@inheritDoc} */
@Override
@@ -366,7 +373,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
return;
}
- final OIDCAuthenticationResponseContext ctx = getOidcResponseContext();
+ final OIDCAuthenticationResponseContext responseCtx = getOidcResponseContext();
final Collection<String> audience = getAudience();
if (audience == null || audience.isEmpty()) {
@@ -374,14 +381,10 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
return;
}
- ctx.getAudience().addAll(audience);
+ responseCtx.getAudience().addAll(audience);
log.debug("{} Building access token with audience: {}", getLogPrefix(), audience);
- final Scope scope = getScope(ctx.getScope());
- if (scope == null) {
- ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
- return;
- }
+ final Scope scope = getScope(responseCtx.getScope());
log.debug("{} Building access token with scope: {}", getLogPrefix(), scope);
final Instant now = Instant.now();
@@ -391,16 +394,16 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
.setJWTID(idGenerator)
.setClientID(clientID)
.setIssuer(issuer)
- .setSubject(ctx.getSubject())
+ .setSubject(responseCtx.getSubject())
.setIssuedAt(now)
.setExpiresAt(dateExp)
- .setACR(ctx.getAcr())
- .setAuthenticationTime(ctx.getAuthTime())
+ .setACR(responseCtx.getAcr())
+ .setAuthenticationTime(responseCtx.getAuthTime())
.setScope(scope)
.setAudience(audience);
- if (ctx.getAccessTokenClaimSet() != null) {
- builder.setCustomClaims(ctx.getAccessTokenClaimSet().toJSONObject());
+ if (responseCtx.getAccessTokenClaimSet() != null) {
+ builder.setCustomClaims(responseCtx.getAccessTokenClaimSet().toJSONObject());
}
final AccessTokenClaimsSet claimsSet = builder.build();
@@ -428,7 +431,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
*
* @return derived {@link Scope} to use or null
*/
- @Nullable private Scope getScope(@Nullable final Scope validatedScope) {
+ @Nonnull private Scope getScope(@Nullable final Scope validatedScope) {
if (scopeAttribute != null) {
final IdPAttribute source = (useUnfilteredAttributes ? attributeCtx.getUnfilteredIdPAttributes()
@@ -441,15 +444,10 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
.map(StringAttributeValue::getValue)
.collect(Collectors.toUnmodifiableList()));
}
-
- log.warn("{} No source attribute {} available to produce scope claim", getLogPrefix(), scopeAttribute);
- return null;
}
- if (validatedScope != null) {
- log.debug("{} Using originally requested/validated scope", getLogPrefix());
- }
- return validatedScope;
+ log.debug("{} Using originally requested/validated scope", getLogPrefix());
+ return validatedScope != null ? validatedScope : new Scope();
}
@Nullable @NonnullElements private Collection<String> getAudience() {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
index a18660b8..a1ba2a4a 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
@@ -24,7 +24,7 @@ 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;
@@ -59,7 +59,7 @@ public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction
// PRC -> inbound message context -> OIDC response context -> ATC
accessTokenContextLookupStrategy = new ChildContextLookup<>(AccessTokenContext.class, true).compose(
new ChildContextLookup<>(OIDCAuthenticationResponseContext.class).compose(
- new InboundMessageContextLookup()));
+ new OutboundMessageContextLookup()));
}
/**
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
index ddbebd8a..3f2c0a23 100644
--- 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
@@ -27,7 +27,7 @@ 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.opensaml.profile.context.navigate.OutboundMessageContextLookup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.nimbusds.jwt.JWTClaimsSet;
@@ -67,7 +67,7 @@ public class SignAccessToken extends AbstractSignJWTAction {
// PRC -> inbound message context -> OIDC response context -> ATC
accessTokenContextLookupStrategy = new ChildContextLookup<>(AccessTokenContext.class, true).compose(
new ChildContextLookup<>(OIDCAuthenticationResponseContext.class).compose(
- new InboundMessageContextLookup()));
+ new OutboundMessageContextLookup()));
}
/**
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessage.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessage.java
index 6cdff686..7be57cc0 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessage.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessage.java
@@ -26,14 +26,18 @@ import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.nimbusds.jwt.JWT;
+import com.nimbusds.oauth2.sdk.AccessTokenResponse;
import com.nimbusds.oauth2.sdk.TokenResponse;
import com.nimbusds.oauth2.sdk.token.AccessToken;
+import com.nimbusds.oauth2.sdk.token.Tokens;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
/**
* Action that forms outbound message based on token request and response context. Formed message is set to
* {@link ProfileRequestContext#getOutboundMessageContext()}.
+ *
+ * <p>This is both OIDC and OAuth aware, based on the presence of an ID token in the context.</p>
*/
public class FormOutboundTokenResponseMessage extends AbstractOIDCTokenResponseAction {
@@ -43,7 +47,7 @@ public class FormOutboundTokenResponseMessage extends AbstractOIDCTokenResponseA
/** access token for response. */
@Nullable private AccessToken accessToken;
- /** id token for response. */
+ /** ID token for response. */
@Nullable private JWT idToken;
/** {@inheritDoc} */
@@ -51,29 +55,33 @@ public class FormOutboundTokenResponseMessage extends AbstractOIDCTokenResponseA
protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
if (!super.doPreExecute(profileRequestContext)) {
- log.error("{} pre-execute failed", getLogPrefix());
return false;
}
+
accessToken = getOidcResponseContext().getAccessToken();
if (accessToken == null) {
- log.error("{} unable to provide access token (required)", getLogPrefix());
+ log.error("{} unable to provide required access token", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return false;
}
+
idToken = getOidcResponseContext().getProcessedToken();
- if (idToken == null) {
- log.error("{} unable to provide id token (required)", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
- return false;
- }
return true;
}
/** {@inheritDoc} */
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- final TokenResponse resp =
- new OIDCTokenResponse(new OIDCTokens(idToken, accessToken, getOidcResponseContext().getRefreshToken()));
+
+ final TokenResponse resp;
+ if (idToken != null) {
+ resp = new OIDCTokenResponse(
+ new OIDCTokens(idToken, accessToken, getOidcResponseContext().getRefreshToken()));
+ } else {
+ // No refresh tokens likely here, but just in case.
+ resp = new AccessTokenResponse(new Tokens(accessToken, getOidcResponseContext().getRefreshToken()));
+ }
profileRequestContext.getOutboundMessageContext().setMessage(resp);
}
+
}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-beans.xml
index 78c7327f..24e4a783 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-beans.xml
@@ -31,12 +31,13 @@
value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
<entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).NO_POTENTIAL_FLOW}"
value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
-
<entry key="#{T(net.shibboleth.idp.profile.IdPEventIds).INVALID_PROFILE_CONFIG}"
- value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).ACCESS_DENIED}" />
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).UNAUTHORIZED_CLIENT}" />
<entry key="#{T(org.opensaml.profile.action.EventIds).ACCESS_DENIED}"
- value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).ACCESS_DENIED}" />
-
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).ACCESS_DENIED}" />
+
+ <entry key="#{T(net.shibboleth.idp.plugin.oidc.op.profile.OidcEventIds).INVALID_GRANT_TYPE}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).UNAUTHORIZED_CLIENT}" />
<entry key="#{T(net.shibboleth.idp.plugin.oidc.op.profile.OidcEventIds).INVALID_GRANT}"
value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_GRANT}" />
<entry key="#{T(net.shibboleth.idp.plugin.oidc.op.profile.OidcEventIds).INVALID_REDIRECT_URI}"
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 984ca005..193c182b 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
@@ -66,14 +66,14 @@
<bean id="NotClientCredentialsGrantCondition" parent="shibboleth.Conditions.NOT">
<constructor-arg>
<bean class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.RequestedGrantTypesCondition"
- p:grantTypes="T(com.nimbusds.oauth2.sdk.GrantType).CLIENT_CREDENTIALS" />
+ p:grantTypes="#{T(com.nimbusds.oauth2.sdk.GrantType).CLIENT_CREDENTIALS}" />
</constructor-arg>
</bean>
<!-- Condition signaling that request was for authorizaton_code grant. -->
<bean id="AuthorizationCodeGrantCondition"
class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.RequestedGrantTypesCondition"
- p:grantTypes="T(com.nimbusds.oauth2.sdk.GrantType).AUTHORIZATION_CODE" />
+ p:grantTypes="#{T(com.nimbusds.oauth2.sdk.GrantType).AUTHORIZATION_CODE}" />
<!-- Traditional third-party grant handling. -->
@@ -268,18 +268,25 @@
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" />
+ class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.AccessTokenClaimsSetLookupFunction"
+ p:autoCreate="true" />
<bean id="BuildAccessToken"
class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.BuildAccessToken" scope="prototype"
- p:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
+ p:dataSealer="#{getObject('%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim())}"
p:useUnfilteredAttributes="%{idp.oauth.accessToken.useUnfilteredAttributes:true}"
p:scopeAttribute="#{'%{idp.oauth.accessToken.scopeAttribute:scope}'.trim()}"
p:audienceAttribute="#{'%{idp.oauth.accessToken.audienceAttribute:audience}'.trim()}" />
<bean id="SignAccessToken"
- class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SignAccessToken" scope="prototype" />
-
+ class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SignAccessToken" scope="prototype">
+ <property name="securityParametersLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookup.SecurityParameters"
+ c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+ </property>
+ </bean>
+
<bean id="SetOAuthAccessTokenToResponseContext"
class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SetAccessTokenToResponseContext"
scope="prototype" />
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
index 4faef1cf..b345a871 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
@@ -121,6 +121,7 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
action.setClientIDLookupStrategy(FunctionSupport.constant(null));
action.setScopeAttribute(null);
action.setAudienceAttribute(null);
+ action.setDataSealer(getDataSealer());
action.initialize();
final Event event = action.execute(requestCtx);
@@ -194,7 +195,9 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
attributeCtx.setUnfilteredIdPAttributes(Collections.singletonList(new IdPAttribute("foo")));
final Event event = action.execute(requestCtx);
- ActionTestingSupport.assertEvent(event, EventIds.ACCESS_DENIED);
+ ActionTestingSupport.assertProceedEvent(event);
+ verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
+ Collections.singletonList("https://rp.example.org"));
}
/**
@@ -376,7 +379,7 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
}
/**
- * Verify opaque access token's claims.
+ * Verify access token's claims.
*
* @param ctx access token context
* @param scope scope to check for
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
index c159f32d..5f4937fd 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
@@ -23,9 +23,9 @@ import java.net.URISyntaxException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
-import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
@@ -186,8 +186,8 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
final ClientAuthenticationMethod tokenEndpointMethod, final JWSAlgorithm userInfoSigAlg,
final RSAPublicKey publicKey, final String... redirectUri) throws IOException {
final OIDCClientMetadata metadata = new OIDCClientMetadata();
- metadata.setGrantTypes(new HashSet<GrantType>(Arrays.asList(GrantType.AUTHORIZATION_CODE,
- GrantType.REFRESH_TOKEN)));
+ metadata.setGrantTypes(new HashSet<GrantType>(List.of(GrantType.AUTHORIZATION_CODE,
+ GrantType.REFRESH_TOKEN, GrantType.CLIENT_CREDENTIALS)));
final HashSet<URI> uris = new HashSet<>();
for (final String uri : redirectUri) {
try {
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/ClientCredentialsTokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/ClientCredentialsTokenFlowTest.java
new file mode 100644
index 00000000..c12233cb
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/ClientCredentialsTokenFlowTest.java
@@ -0,0 +1,353 @@
+/*
+ * 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.flow;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.io.IOException;
+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.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.storage.StorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.AccessTokenResponse;
+import com.nimbusds.oauth2.sdk.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.GrantType;
+import com.nimbusds.oauth2.sdk.OAuth2Error;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretJWT;
+import com.nimbusds.oauth2.sdk.auth.JWTAuthentication;
+import com.nimbusds.oauth2.sdk.pkce.CodeChallenge;
+import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod;
+import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
+import com.nimbusds.oauth2.sdk.token.AccessToken;
+import com.nimbusds.oauth2.sdk.token.RefreshToken;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.minidev.json.JSONObject;
+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.profile.impl.ValidateGrantTest;
+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.collection.Pair;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.security.DataSealerException;
+
+/**
+ * Unit tests for the token flow when using the client_credentials grant type.
+ */
+public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
+
+ public static final String FLOW_ID = "oidc/token";
+
+ private final Scope scope = Scope.parse("profile email");
+
+ private final String resource = "https://rp.example.org";
+
+ @Autowired
+ @Qualifier("shibboleth.StorageService")
+ StorageService storageService;
+
+ public ClientCredentialsTokenFlowTest() {
+ super(FLOW_ID);
+ }
+
+ @AfterMethod
+ public void removeMetadata() throws IOException {
+ removeMetadata(storageService, clientId);
+ }
+
+ @Test
+ public void testUntrustedClient() throws IOException, ParseException {
+ setHttpFormRequest("POST", createRequestParameters(clientId + "2", scope, resource));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ // This is invalid_request because Nimbus bails out too early if client creds are missing.
+ assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+ assertErrorDescriptionContains(result, "UnableToDecode");
+ }
+
+ @Test
+ public void testInvalidClientSecret() throws ParseException, IOException {
+ setHttpFormRequest("POST", createRequestParameters(clientId, scope, resource));
+ storeMetadata(storageService, clientId, clientSecret, scope);
+ setBasicAuth(clientId, clientSecret + "2");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_CLIENT_CODE);
+ }
+
+ @Test
+ public void testInvalidGrantType() throws ParseException, IOException {
+ setHttpFormRequest("POST", createRequestParameters(clientId, scope, resource));
+ setBasicAuth(clientIdSaml, clientSecretSaml);
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.UNAUTHORIZED_CLIENT_CODE);
+ }
+
+ @Test
+ public void testNoScopes() throws Exception {
+ setHttpFormRequest("POST", createRequestParameters(clientId, scope, resource));
+ storeMetadata(storageService, clientId, clientSecret, null);
+ setBasicAuth(clientId, clientSecret);
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getBearerAccessToken());
+ Assert.assertEquals(response.getTokens().getBearerAccessToken().getLifetime(), 600);
+ Assert.assertNull(response.getTokens().getBearerAccessToken().getScope());
+ verifyClaims(null, response.getTokens().getBearerAccessToken(), new Scope(),
+ Collections.singletonList(resource));
+ }
+
+ @Test
+ public void testNoScopesJWT() throws Exception {
+ setHttpFormRequest("POST", createRequestParameters(clientId + "JWT", scope, resource));
+ storeMetadata(storageService, clientId + "JWT", clientSecret, null);
+ setBasicAuth(clientId + "JWT", clientSecret);
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getBearerAccessToken());
+ Assert.assertEquals(response.getTokens().getBearerAccessToken().getLifetime(), 600);
+ Assert.assertNull(response.getTokens().getBearerAccessToken().getScope());
+ verifyClaims("JWT", response.getTokens().getBearerAccessToken(), new Scope(),
+ Collections.singletonList(resource));
+ }
+
+ protected void initializeGrantAndRequest(final String clientId, final Map<String, String> requestParameters)
+ throws IOException {
+ setHttpFormRequest("POST", requestParameters);
+ storeMetadata(storageService, clientId, clientSecret, scope);
+ setBasicAuth(clientId, clientSecret);
+ }
+
+ /*
+ @Test
+ public void testValidGrant() throws Exception {
+ initializeGrantAndRequest(clientId, createRequestParameters(clientId, scope, resource));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getAccessToken());
+ }
+
+ @Test
+ public void testValidGrantWithRequestedScope() throws Exception {
+ final Map<String,String> params = createRequestParameters(clientId, scope, resource);
+ params.put("scope", "openid profile");
+ initializeGrantAndRequest(clientId, params);
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getAccessToken());
+
+ final ValidateGrantTest test = new ValidateGrantTest();
+ final AccessTokenClaimsSet token =
+ AccessTokenClaimsSet.parse(response.getTokens().getAccessToken().getValue(), test.getDataSealer());
+ Assert.assertTrue(token.getScope().contains("openid"));
+ Assert.assertTrue(token.getScope().contains("profile"));
+ Assert.assertFalse(token.getScope().contains("email"));
+ }
+
+ @Test
+ public void testValidSecretJWT() throws Exception {
+ final ClientSecretJWT clientAuth = buildSecretJwtAuth(clientSecret);
+ final FlowExecutionResult result = launchWithJwtAuthentication(clientAuth, JWSAlgorithm.HS256);
+ final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getAccessToken());
+ }
+
+ @Test
+ public void testValidSecretJWTNoAlg() throws Exception {
+ final ClientSecretJWT clientAuth = buildSecretJwtAuth(clientSecret);
+ final FlowExecutionResult result = launchWithJwtAuthentication(clientAuth, null);
+ final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getAccessToken());
+ }
+
+ @Test
+ public void testInvalidSecretJWT() throws Exception {
+ final ClientSecretJWT clientAuth = buildSecretJwtAuth(clientSecret + "invalid");
+ final FlowExecutionResult result = launchWithJwtAuthentication(clientAuth, JWSAlgorithm.HS256);
+ assertErrorCode(result, OAuth2Error.INVALID_CLIENT_CODE);
+ }
+
+ @Test
+ public void testValidGrantWrappedClaimsUI() throws Exception {
+ final String claimName = "name";
+ final String claimValue = "John Doe";
+ final JSONObject claimsUI = new JSONObject();
+ claimsUI.put(claimName, claimValue);
+ initializeGrantAndRequest(clientId, createRequestParameters(clientId, scope, resource));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+ final AccessTokenClaimsSet claimsSet = unwrapAccessToken(response);
+ Assert.assertNotNull(claimsSet);
+ final ClaimsSet dlClaimsSet = claimsSet.getUserinfoDeliveryClaims();
+ Assert.assertNotNull(dlClaimsSet);
+ Assert.assertEquals(dlClaimsSet.getClaim(claimName), claimValue);
+ Assert.assertNull(claimsSet.getDeliveryClaims().getClaim(claimName));
+ Assert.assertNull(claimsSet.getIDTokenDeliveryClaims());
+ }
+
+ @Test
+ public void testValidGrantWrappedClaims() throws Exception {
+ final String claimName = "name";
+ final String claimValue = "John Doe";
+ final JSONObject claims = new JSONObject();
+ claims.put(claimName, claimValue);
+ initializeGrantAndRequest(clientId, createRequestParameters(clientId, scope, resource));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+ final AccessTokenClaimsSet claimsSet = unwrapAccessToken(response);
+ Assert.assertNotNull(claimsSet);
+ final ClaimsSet dlClaimsSet = claimsSet.getDeliveryClaims();
+ Assert.assertNotNull(dlClaimsSet);
+ Assert.assertEquals(dlClaimsSet.getClaim(claimName), claimValue);
+ Assert.assertNull(claimsSet.getUserinfoDeliveryClaims().getClaim(claimName));
+ Assert.assertNull(claimsSet.getIDTokenDeliveryClaims());
+ }
+ */
+
+ private AccessTokenClaimsSet unwrapAccessToken(final AccessTokenResponse tokenResponse) {
+ final AccessToken accessToken = tokenResponse.getTokens().getAccessToken();
+ Assert.assertNotNull(accessToken);
+ try {
+ return AccessTokenClaimsSet.parse(accessToken.getValue(),
+ BaseOIDCResponseActionTest.initializeDataSealer());
+ } catch (final NoSuchAlgorithmException | java.text.ParseException | DataSealerException
+ | ComponentInitializationException e) {
+ return null;
+ }
+ }
+
+ protected FlowExecutionResult launchWithJwtAuthentication(final JWTAuthentication authnMethod, final JWSAlgorithm algorithm)
+ throws Exception {
+ storeMetadata(storageService, clientId, clientSecret, scope, JWSAlgorithm.HS256,
+ ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+ final Map<String, String> requestParameters = createRequestParameters(clientId, scope, resource);
+ populateClientAssertionParams(requestParameters, authnMethod);
+ setHttpFormRequest("POST", requestParameters);
+ return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ }
+
+ protected FlowExecutionResult launchWithJwtAuthentication(final SignedJWT jwt, final JWSAlgorithm algorithm,
+ final ClientAuthenticationMethod method) throws Exception {
+ if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.equals(method)) {
+ storeMetadata(storageService, clientId, clientSecret, scope, algorithm, method);
+ } else {
+ storeMetadata(storageService, clientId, null, scope, algorithm, method, null, rsaPublicKey);
+ }
+ final Map<String, String> requestParameters = createRequestParameters(clientId, scope, resource);
+ populateClientAssertionParams(requestParameters, jwt);
+ setHttpFormRequest("POST", requestParameters);
+ return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ }
+
+ protected Map<String, String> createRequestParameters(final String clientId, final Scope scope, final String resource) {
+ final Map<String, String> parameters = new HashMap<>();
+ addNonNullValue(parameters, "grant_type", GrantType.CLIENT_CREDENTIALS.getValue());
+ if (scope != null) {
+ addNonNullValue(parameters, "scope", scope.toString());
+ }
+ if (resource != null) {
+ addNonNullValue(parameters, "resource", resource);
+ }
+ return parameters;
+ }
+
+ private void addNonNullValue(final Map<String, String> map, final String key, final String value) {
+ if (value != null) {
+ map.put(key, value);
+ }
+ }
+
+ protected Pair<String, String> getErrorDetaisForJWTValidation() {
+ return new Pair<>("invalid_client", "Client authentication failed");
+ }
+
+ /**
+ * Verify access token's claims.
+ *
+ * @param type token type/format
+ * @param token access token
+ * @param s scope to check for
+ * @param audiences audiences to check for
+ *
+ * @throws ComponentInitializationException
+ * @throws DataSealerException
+ * @throws ParseException
+ * @throws NoSuchAlgorithmException
+ */
+ private void verifyClaims(@Nullable final String type, @Nonnull final AccessToken token, @Nonnull final Scope s,
+ @Nonnull @NonnullElements final Collection<String> audiences)
+ throws NoSuchAlgorithmException, ParseException, DataSealerException, ComponentInitializationException {
+
+ if (type == null) {
+ final AccessTokenClaimsSet at = AccessTokenClaimsSet.parse(token.getValue(),
+ BaseOIDCResponseActionTest.initializeDataSealer());
+ assertNotNull(at);
+ assertEquals(at.getACR(), null);
+ 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(), "https://op.example.org");
+ assertTrue(at.getIssuedAt().isBefore(Instant.now()));
+ assertEquals(at.getScope(), s);
+ assertEquals(at.getSubject(), clientId);
+ } else if ("JWT".equals(type)) {
+ final JWTClaimsSet claims = SignedJWT.parse(token.getValue()).getJWTClaimsSet();
+ assertNotNull(claims);
+ assertEquals(claims.getClaim(TokenClaimsSet.KEY_ACR), null);
+ assertEquals(claims.getAudience(), audiences);
+ assertTrue(claims.getDateClaim(TokenClaimsSet.KEY_AUTH_TIME).toInstant().isBefore(Instant.now()));
+ assertEquals(claims.getStringClaim(TokenClaimsSet.KEY_CLIENTID), clientId + type);
+ assertEquals(claims.getExpirationTime().toInstant(), claims.getIssueTime().toInstant().plusSeconds(600));
+ assertEquals(claims.getIssuer(), "https://op.example.org");
+ assertTrue(claims.getIssueTime().toInstant().isBefore(Instant.now()));
+ assertEquals(claims.getStringClaim(TokenClaimsSet.KEY_SCOPE), s.toString());
+ assertEquals(claims.getSubject(), clientId + type);
+ } else {
+ throw new RuntimeException("Bad token format");
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
index fe06694f..2ca1a8fc 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
@@ -81,7 +81,7 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
setHttpFormRequest("POST", Collections.singletonMap("token",
super.buildToken(clientId, "sub", Scope.parse("openid")).toJSONObject().getAsString("access_token")));
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertErrorCode(result, OAuth2Error.ACCESS_DENIED_CODE);
+ assertErrorCode(result, OAuth2Error.UNAUTHORIZED_CLIENT_CODE);
}
@Test
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
index f2658e05..eb84b277 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
@@ -80,7 +80,7 @@ public class RevocationFlowTest extends AbstractOidcClientAuthenticationFlowTest
setHttpFormRequest("POST", Collections.singletonMap("token", super.buildToken(clientId, "sub",
Scope.parse("openid")).toJSONObject().getAsString("access_token")));
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertErrorCode(result, OAuth2Error.ACCESS_DENIED_CODE);
+ assertErrorCode(result, OAuth2Error.UNAUTHORIZED_CLIENT_CODE);
}
@Test
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
index 2e2127d9..812a4f3d 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
@@ -108,7 +108,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
public void testUntrustedClient() throws IOException, ParseException {
setHttpFormRequest("POST", createRequestParameters(null, "authorization_code", "mockCode", clientId + "2"));
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertErrorCode(result, OAuth2Error.ACCESS_DENIED_CODE);
+ assertErrorCode(result, OAuth2Error.UNAUTHORIZED_CLIENT_CODE);
}
@Test
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessageTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessageTest.java
index af3ec28f..acc15a8d 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessageTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessageTest.java
@@ -21,7 +21,6 @@ import java.net.URISyntaxException;
import java.time.Duration;
import java.time.Instant;
-import net.shibboleth.idp.plugin.oidc.op.profile.impl.FormOutboundTokenResponseMessage;
import net.shibboleth.idp.profile.testing.ActionTestingSupport;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import org.opensaml.messaging.context.MessageContext;
@@ -34,11 +33,22 @@ import com.nimbusds.jose.JOSEException;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.TokenResponse;
+// Checkstyle: ThrowsCount OFF
+
/** {@link FormOutboundTokenResponseMessage} unit test. */
public class FormOutboundTokenResponseMessageTest extends BaseOIDCResponseActionTest {
+ /** Action to test. */
private FormOutboundTokenResponseMessage action;
+ /**
+ * Init method.
+ *
+ * @throws ComponentInitializationException
+ * @throws URISyntaxException
+ * @throws ParseException
+ * @throws JOSEException
+ */
@BeforeMethod
public void init() throws ComponentInitializationException, URISyntaxException, ParseException, JOSEException {
action = new FormOutboundTokenResponseMessage();
@@ -50,9 +60,14 @@ public class FormOutboundTokenResponseMessageTest extends BaseOIDCResponseAction
/**
* Test that action is able to form success message.
+ *
+ * @throws ComponentInitializationException
+ * @throws URISyntaxException
+ * @throws ParseException
+ * @throws JOSEException
*/
@Test
- public void testSuccessMessage()
+ public void testOIDCSuccess()
throws ComponentInitializationException, URISyntaxException, ParseException, JOSEException {
final Event event = action.execute(requestCtx);
ActionTestingSupport.assertProceedEvent(event);
@@ -61,17 +76,27 @@ public class FormOutboundTokenResponseMessageTest extends BaseOIDCResponseAction
/**
* Test that action fails if there is no id token.
+ *
+ * @throws ComponentInitializationException
+ * @throws URISyntaxException
+ * @throws ParseException
+ * @throws JOSEException
*/
@Test
- public void testFailNoIdToken()
+ public void testOAuthSuccess()
throws ComponentInitializationException, URISyntaxException, ParseException, JOSEException {
respCtx.setProcessedToken(null);
final Event event = action.execute(requestCtx);
- ActionTestingSupport.assertEvent(event, EventIds.INVALID_PROFILE_CTX);
+ Assert.assertTrue(((MessageContext) respCtx.getParent()).getMessage() instanceof TokenResponse);
}
/**
* Test that action fails if there is no access token.
+ *
+ * @throws ComponentInitializationException
+ * @throws URISyntaxException
+ * @throws ParseException
+ * @throws JOSEException
*/
@Test
public void testFailNoAccessToken()
diff --git a/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml b/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml
index 197a3f27..131ab679 100644
--- a/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml
+++ b/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml
@@ -83,6 +83,30 @@
<AttributeEncoder xsi:type="oidc:OIDCString" name="sub" />
</AttributeDefinition>
+ <!-- Used to drive client_credentials grant handling. -->
+
+ <AttributeDefinition id="requestedScope" xsi:type="ScriptedAttribute" resolutionPhases="oidc/token">
+ <Script>
+ <![CDATA[
+ requestedScope.getValues().addAll(
+ resolutionContext.getSubcontext(
+ "net.shibboleth.idp.plugin.oidc.op.profile.context.OAuthAttributeResolutionContext").getScope()
+ );
+ ]]>
+ </Script>
+ </AttributeDefinition>
+
+ <AttributeDefinition id="requestedAudience" xsi:type="ScriptedAttribute" resolutionPhases="oidc/token">
+ <Script>
+ <![CDATA[
+ requestedAudience.getValues().addAll(
+ resolutionContext.getSubcontext(
+ "net.shibboleth.idp.plugin.oidc.op.profile.context.OAuthAttributeResolutionContext").getResources()
+ );
+ ]]>
+ </Script>
+ </AttributeDefinition>
+
<!-- ========================================== -->
<!-- Data Connectors -->
diff --git a/idp-oidc-extension-impl/src/test/resources/conf/oidc.properties b/idp-oidc-extension-impl/src/test/resources/conf/oidc.properties
index 0d00d608..557b62fd 100644
--- a/idp-oidc-extension-impl/src/test/resources/conf/oidc.properties
+++ b/idp-oidc-extension-impl/src/test/resources/conf/oidc.properties
@@ -6,4 +6,7 @@ idp.oidc.subject.sourceAttribute = uid
idp.oidc.subject.salt = isfd07fsddfs70sdf9d99s8
idp.oidc.discovery.template = src/test/resources/conf/openid-configuration.json
-idp.oidc.dynreg.defaultMetadataPolicyFile = src/test/resources/conf/metadata-policy1.json
\ No newline at end of file
+idp.oidc.dynreg.defaultMetadataPolicyFile = src/test/resources/conf/metadata-policy1.json
+
+idp.oauth.accessToken.scopeAttribute = requestedScope
+idp.oauth.accessToken.audienceAttribute = requestedAudience
diff --git a/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
index 5eb74533..b25d62e5 100644
--- a/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
@@ -78,6 +78,13 @@
</list>
</property>
</bean>
+ <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdJWT">
+ <property name="profileConfigurations">
+ <list>
+ <bean parent="OIDC.Token.MDDriven" p:accessTokenType="JWT" />
+ </list>
+ </property>
+ </bean>
</util:list>
</beans>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list