[java-idp-oidc] branch main updated: JOIDC-186 - Support additional refresh token types
Henri Mikkonen
henri.mikkonen at iki.fi
Fri Jan 19 06:12:14 UTC 2024
This is an automated email from the git hooks/post-receive script.
hjmikkon 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=bc022b4fd326e0a62f425a00764bd4666b0b70a0
The following commit(s) were added to refs/heads/main by this push:
new bc022b4f JOIDC-186 - Support additional refresh token types
bc022b4f is described below
commit bc022b4fd326e0a62f425a00764bd4666b0b70a0
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Jan 19 08:11:28 2024 +0200
JOIDC-186 - Support additional refresh token types
https://shibboleth.atlassian.net/browse/JOIDC-186
Two new beans wired to the actions dealing with refresh tokens
shibboleth.oidc.DefaultRefreshTokenSerializationStrategies (in token beans)
- Map of serialization strategies (keyed with the refresh token type)
- Serializer: BiFunction<ProfileRequestContext,RefreshTokenClaimsSet,String>
shibboleth.oidc.DefaultRefreshTokenDeserializers (in abstract beans)
- List of deserializers
- Deserializer: BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>
- Used by token, introspection and revocation flow
---
.../profile/impl/AbstractProcessTokenAction.java | 111 +++++----
.../impl/SetRefreshTokenToResponseContext.java | 89 ++++++--
.../plugin/oidc/op/profile/impl/ValidateGrant.java | 117 ++++++----
...aultJwtRefreshTokenDeserializationFunction.java | 220 ++++++++++++++++++
...efaultJwtRefreshTokenSerializationFunction.java | 251 +++++++++++++++++++++
.../oauth2/introspection/introspection-beans.xml | 3 +-
.../flows/oauth2/revocation/revocation-beans.xml | 3 +-
.../oidc/abstract-api/oidc-abstract-api-beans.xml | 49 ++++
.../idp/flows/oidc/token/token-beans.xml | 56 ++++-
.../op/profile/flow/AbstractOidcApiFlowTest.java | 26 +++
.../op/profile/flow/IntrospectionFlowTest.java | 41 ++++
.../oidc/op/profile/flow/RevocationFlowTest.java | 32 +++
.../plugin/oidc/op/profile/flow/TokenFlowTest.java | 108 +++++++--
.../impl/SetRefreshTokenToResponseContextTest.java | 80 +++++++
.../oidc/op/profile/impl/ValidateGrantTest.java | 51 ++++-
...JwtRefreshTokenDeserializationFunctionTest.java | 205 +++++++++++++++++
...ltJwtRefreshTokenSerializationFunctionTest.java | 205 +++++++++++++++++
.../shibboleth/idp/module/conf/relying-party.xml | 11 +
18 files changed, 1537 insertions(+), 121 deletions(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/AbstractProcessTokenAction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/AbstractProcessTokenAction.java
index 1af9a2b1..e56f8692 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/AbstractProcessTokenAction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/AbstractProcessTokenAction.java
@@ -17,6 +17,8 @@ package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.List;
+import java.util.function.BiFunction;
import java.util.function.Function;
import javax.annotation.Nonnull;
@@ -51,6 +53,7 @@ import net.shibboleth.oidc.profile.config.navigate.IssuedClaimsValidatorLookupFu
import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.oidc.security.impl.JWTSignatureValidationUtil;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.resolver.CriteriaSet;
import net.shibboleth.shared.resolver.ResolverException;
@@ -89,12 +92,16 @@ public abstract class AbstractProcessTokenAction<T> extends AbstractOIDCRequestA
/** Source of signing keys. */
@Nullable private CredentialResolver credentialResolver;
+ /** List of deserializer bi-functions for refresh tokens to be used in addition to unsealing opaque value. */
+ @Nonnull private List<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>> refreshTokenDeserializers;
+
/** Copy of signed JWT for non-opaque access tokens. */
@Nullable private SignedJWT signedJWT;
/** Constructor. */
public AbstractProcessTokenAction() {
claimsValidatorLookupStrategy = new IssuedClaimsValidatorLookupFunction();
+ refreshTokenDeserializers = CollectionSupport.emptyList();
}
/**
@@ -128,6 +135,18 @@ public abstract class AbstractProcessTokenAction<T> extends AbstractOIDCRequestA
credentialResolver = resolver;
}
+ /**
+ * Set the list of deserializer bi-functions for refresh tokens to be used in addition to unsealing opaque value.
+ *
+ * @param deserializers list of deserializers
+ */
+ public void setRefreshTokenDeserializers(
+ @Nonnull final List<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>> deserializers) {
+ checkSetterPreconditions();
+ refreshTokenDeserializers =
+ Constraint.isNotNull(deserializers, "List of refresh token deserializers cannot be null");
+ }
+
/** {@inheritDoc} */
@Override
protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -162,12 +181,12 @@ public abstract class AbstractProcessTokenAction<T> extends AbstractOIDCRequestA
if (token instanceof AccessToken) {
tokenClaimsSet = parseAccessToken(token);
} else if (token instanceof RefreshToken) {
- tokenClaimsSet = parseRefreshToken(token);
+ tokenClaimsSet = parseRefreshToken(profileRequestContext, token);
} else {
// No token hint, have to try both.
tokenClaimsSet = parseAccessToken(token);
if (tokenClaimsSet == null) {
- tokenClaimsSet = parseRefreshToken(token);
+ tokenClaimsSet = parseRefreshToken(profileRequestContext, token);
}
}
@@ -176,40 +195,6 @@ public abstract class AbstractProcessTokenAction<T> extends AbstractOIDCRequestA
return;
}
- if (signedJWT != null) {
- // Check typ header.
- final JOSEObjectType typ = signedJWT.getHeader().getType();
- if (typ == null || !"at+jwt".equals(typ.getType())) {
- log.warn("{} Missing or invalid token type: {}", getLogPrefix(), typ != null ? typ.getType() : "null");
- return;
- }
-
- if (credentialResolver == null) {
- log.error("{} No CredentialResolver available, can't verify JWT signature", getLogPrefix());
- return;
- }
-
- log.debug("{} Checking JWT signature", getLogPrefix());
- final Collection<Credential> credList = new ArrayList<>();
- final CriteriaSet criteriaSet = new CriteriaSet(new UsageCriterion(UsageType.SIGNING));
- try {
- final Iterable<Credential> creds = credentialResolver.resolve(criteriaSet);
- if (creds != null) {
- creds.forEach(credList::add);
- }
- } catch (final ResolverException e) {
- log.error("{} Failure resolving signing credentials, can't verify JWT signature", getLogPrefix(), e);
- return;
- }
-
- final String errorEventId = JWTSignatureValidationUtil.validateSignatureEx(credList, signedJWT,
- OidcEventIds.INVALID_GRANT);
- if (errorEventId != null) {
- log.warn("{} Signature on token ID '{}' invalid", getLogPrefix(), tokenClaimsSet.getJWTID());
- return;
- }
- }
-
log.debug("{} Validating parsed/decoded claims set: {}", getLogPrefix(), tokenClaimsSet.toString());
try {
claimsValidator.validate(tokenClaimsSet, profileRequestContext);
@@ -236,7 +221,39 @@ public abstract class AbstractProcessTokenAction<T> extends AbstractOIDCRequestA
// Try parsing as a JWT.
try {
signedJWT = SignedJWT.parse(token.getValue());
- return signedJWT.getJWTClaimsSet();
+ if (signedJWT != null) {
+ // Check typ header.
+ final JOSEObjectType typ = signedJWT.getHeader().getType();
+ if (typ != null && "at+jwt".equals(typ.getType())) {
+ if (credentialResolver == null) {
+ log.error("{} No CredentialResolver available, can't verify JWT signature", getLogPrefix());
+ return null;
+ }
+
+ final JWTClaimsSet jwtClaimsSet = signedJWT.getJWTClaimsSet();
+ log.debug("{} Checking JWT signature", getLogPrefix());
+ final Collection<Credential> credList = new ArrayList<>();
+ final CriteriaSet criteriaSet = new CriteriaSet(new UsageCriterion(UsageType.SIGNING));
+ try {
+ final Iterable<Credential> creds = credentialResolver.resolve(criteriaSet);
+ creds.forEach(credList::add);
+ } catch (final ResolverException e) {
+ log.error("{} Failure resolving signing credentials, can't verify JWT signature", getLogPrefix(), e);
+ return null;
+ }
+
+ final String errorEventId = JWTSignatureValidationUtil.validateSignatureEx(credList, signedJWT,
+ OidcEventIds.INVALID_GRANT);
+ if (errorEventId != null) {
+ log.warn("{} Signature on token ID '{}' invalid", getLogPrefix(), jwtClaimsSet.getJWTID());
+ return null;
+ }
+ return jwtClaimsSet;
+ } else {
+ log.warn("{} Missing or invalid token type: {}", getLogPrefix(), typ != null ? typ.getType() : "null");
+ return null;
+ }
+ }
} catch (final ParseException e1) {
}
@@ -258,15 +275,21 @@ public abstract class AbstractProcessTokenAction<T> extends AbstractOIDCRequestA
*
* @return parsed claim set or null
*/
- @Nullable protected JWTClaimsSet parseRefreshToken(@Nonnull @NotEmpty final Token token) {
-
- // All refresh tokens are opaque.
+ @Nullable protected JWTClaimsSet parseRefreshToken(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull @NotEmpty final Token token) {
+ final String refreshToken = token.getValue();
try {
- return RefreshTokenClaimsSet.parse(token.getValue(), dataSealer).getClaimsSet();
- } catch (final DataSealerException | ParseException e) {
-
+ assert dataSealer != null;
+ return RefreshTokenClaimsSet.parse(refreshToken, dataSealer).getClaimsSet();
+ } catch (ParseException | DataSealerException e) {
+ }
+ for (final BiFunction<ProfileRequestContext, String, RefreshTokenClaimsSet> deserializer :
+ refreshTokenDeserializers) {
+ final RefreshTokenClaimsSet deserializedSet = deserializer.apply(profileRequestContext, refreshToken);
+ if (deserializedSet != null) {
+ return deserializedSet.getClaimsSet();
+ }
}
-
return null;
}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
index 0589d035..e3cf0379 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
@@ -44,8 +44,10 @@ import net.shibboleth.oidc.profile.config.logic.EnforceRefreshTokenRotationPredi
import net.shibboleth.oidc.profile.config.navigate.RefreshTokenChainLifetimeLookupFunction;
import net.shibboleth.oidc.profile.config.navigate.RefreshTokenClaimsSetManipulationStrategyLookupFunction;
import net.shibboleth.oidc.profile.config.navigate.RefreshTokenTimeoutLookupFunction;
+import net.shibboleth.oidc.profile.config.navigate.RefreshTokenTypeLookupFunction;
import net.shibboleth.shared.annotation.ParameterName;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.logic.FunctionSupport;
@@ -97,6 +99,13 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
/** Lookup function to supply token revocation lifetime. */
@Nonnull private Function<JWTClaimsSet,Duration> tokenRevocationLifetimeLookupStrategy;
+ /** Strategy used to obtain the refresh token type to issue. */
+ @Nonnull private Function<ProfileRequestContext,String> refreshTokenTypeLookupStrategy;
+
+ /** The strategies used for serializing refresh token claims set, key referring to the refresh token type. */
+ @Nonnull private Map<String, BiFunction<ProfileRequestContext,RefreshTokenClaimsSet,String>>
+ refreshTokenSerializationStrategies;
+
/** Authorize Code / Refresh Token the refresh token will be based on. */
@Nullable private TokenClaimsSet tokenClaimsSet;
@@ -109,6 +118,9 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
/** The generator to use. */
@Nullable private IdentifierGenerationStrategy idGenerator;
+ /** Refresh Token type. */
+ @Nullable private String refreshTokenType;
+
/**
* Constructor.
*
@@ -123,6 +135,8 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
enforceRefreshTokenRotationCondition = new EnforceRefreshTokenRotationPredicate();
tokenRevocationLifetimeLookupStrategy = new DefaultTokenRevocationLifetimeLookupStrategy();
+ refreshTokenTypeLookupStrategy = new RefreshTokenTypeLookupFunction();
+ refreshTokenSerializationStrategies = CollectionSupport.emptyMap();
}
/**
@@ -131,7 +145,8 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
* @param cache The revocationCache to set.
*/
public void setRevocationCache(@Nonnull final RevocationCache cache) {
- ifInitializedThrowUnmodifiabledComponentException();
+ checkSetterPreconditions();
+
revocationCache = Constraint.isNotNull(cache, "RevocationCache cannot be null");
}
@@ -142,7 +157,7 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
*/
public void setRefreshTokenChainLifetimeLookupStrategy(
@Nonnull final Function<ProfileRequestContext,Duration> strategy) {
- ifInitializedThrowUnmodifiabledComponentException();
+ checkSetterPreconditions();
refreshTokenChainLifetimeLookupStrategy =
Constraint.isNotNull(strategy, "Refresh token chain lifetime lookup strategy cannot be null");
@@ -155,7 +170,7 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
*/
public void setRefreshTokenTimeoutLookupStrategy(
@Nonnull final Function<ProfileRequestContext,Duration> strategy) {
- ifInitializedThrowUnmodifiabledComponentException();
+ checkSetterPreconditions();
refreshTokenTimeoutLookupStrategy =
Constraint.isNotNull(strategy, "Refresh token timeout lookup strategy cannot be null");
@@ -169,7 +184,7 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
public void setTokenClaimsSetManipulationStrategyLookupStrategy(@Nonnull final
Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
strategy) {
- ifInitializedThrowUnmodifiabledComponentException();
+ checkSetterPreconditions();
tokenClaimsSetManipulationStrategyLookupStrategy =
Constraint.isNotNull(strategy, "Manipulation strategy lookup strategy cannot be null");
@@ -182,7 +197,7 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
*/
public void setIdentifierGeneratorLookupStrategy(
@Nonnull final Function<ProfileRequestContext,IdentifierGenerationStrategy> strategy) {
- ifInitializedThrowUnmodifiabledComponentException();
+ checkSetterPreconditions();
idGeneratorLookupStrategy =
Constraint.isNotNull(strategy, "Identifier generation strategy cannot be null");
@@ -194,7 +209,7 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
* @param condition condition to apply
*/
public void setEnforceRefreshTokenRotationCondition(@Nonnull final Predicate<ProfileRequestContext> condition) {
- ifInitializedThrowUnmodifiabledComponentException();
+ checkSetterPreconditions();
enforceRefreshTokenRotationCondition = Constraint.isNotNull(condition, "Condition cannot be null");
}
@@ -206,9 +221,34 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
*/
public void setTokenRevocationLifetimeLookupStrategy(
@Nullable final Function<JWTClaimsSet,Duration> strategy) {
+ checkSetterPreconditions();
+
tokenRevocationLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
}
+ /**
+ * Set a lookup strategy to obtain the refresh token type to issue.
+ *
+ * @param strategy What to set.
+ */
+ public void setRefreshTokenTypeLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+ checkSetterPreconditions();
+
+ refreshTokenTypeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategies used for serializing refresh token claims set, key referring to the refresh token type.
+ *
+ * @param strategies What to set.
+ */
+ public void setRefreshTokenSerializationStrategies(@Nonnull final
+ Map<String, BiFunction<ProfileRequestContext,RefreshTokenClaimsSet,String>> strategies) {
+ checkSetterPreconditions();
+
+ refreshTokenSerializationStrategies = Constraint.isNotNull(strategies, "Strategies cannot be null");
+ }
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -257,6 +297,8 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
return false;
}
+ refreshTokenType = refreshTokenTypeLookupStrategy.apply(profileRequestContext);
+
return true;
}
@@ -299,15 +341,34 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
} else {
log.debug("{} No manipulation strategy configured", getLogPrefix());
}
-
- try {
- getOidcResponseContext().setRefreshToken(claimsSet.serialize(dataSealer));
- log.debug("{} Setting refresh token {} as {} to response context ", getLogPrefix(), claimsSet.serialize(),
- getOidcResponseContext().getRefreshToken());
- } catch (final DataSealerException e) {
- log.error("{} Refresh Token generation failed {}", getLogPrefix(), e.getMessage());
- ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
+ if (StringSupport.trimOrNull(refreshTokenType) == null) {
+ try {
+ getOidcResponseContext().setRefreshToken(claimsSet.serialize(dataSealer));
+ } catch (final DataSealerException e) {
+ log.error("{} Refresh Token generation failed {}", getLogPrefix(), e.getMessage());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
+ return;
+ }
+ } else {
+ final BiFunction<ProfileRequestContext,RefreshTokenClaimsSet,String> serializationStrategy
+ = refreshTokenSerializationStrategies.get(refreshTokenType);
+ if (serializationStrategy == null) {
+ log.error("{} Could not find a seralization strategy for refresh token type {}", getLogPrefix(),
+ refreshTokenType);
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return;
+ }
+ final String serializedToken = serializationStrategy.apply(profileRequestContext, claimsSet);
+ if (serializedToken == null) {
+ log.error("{} Could not serialize the claims set with refresh token type {}", getLogPrefix(),
+ refreshTokenType);
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return;
+ }
+ getOidcResponseContext().setRefreshToken(serializedToken);
}
+ log.debug("{} Setting refresh token {} as {} to response context ", getLogPrefix(), claimsSet.serialize(),
+ getOidcResponseContext().getRefreshToken());
if (enforceRefreshTokenRotationCondition.test(profileRequestContext) &&
tokenClaimsSet instanceof RefreshTokenClaimsSet) {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
index 2f9b0141..b7ce2e87 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
@@ -17,6 +17,8 @@ package net.shibboleth.idp.plugin.oidc.op.profile.impl;
import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
+import java.util.List;
+import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -50,6 +52,7 @@ import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.profile.context.RelyingPartyContext;
import net.shibboleth.shared.annotation.ParameterName;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.StringSupport;
@@ -97,6 +100,9 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
/** Strategy used to obtain the refresh token lifetime. */
@Nonnull private Function<ProfileRequestContext,Duration> refreshTokenChainLifetimeLookupStrategy;
+ /** List of deserializer bi-functions for refresh tokens to be used in addition to unsealing opaque value. */
+ @Nonnull private List<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>> refreshTokenDeserializers;
+
/** The RelyingPartyContext to operate on. */
@Nullable private RelyingPartyContext rpCtx;
@@ -115,6 +121,7 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
chainRevocationLifetimeLookupStrategy = new DefaultChainRevocationLifetimeLookupStrategy();
((RevocationLifetimeLookupFunction) chainRevocationLifetimeLookupStrategy).setUseActiveProfileOnly(false);
refreshTokenChainLifetimeLookupStrategy = new RefreshTokenChainLifetimeLookupFunction();
+ refreshTokenDeserializers = CollectionSupport.emptyList();
}
/**
@@ -186,6 +193,18 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
Constraint.isNotNull(strategy, "Refresh token chain lifetime lookup strategy cannot be null");
}
+ /**
+ * Set the list of deserializer bi-functions for refresh tokens to be used in addition to unsealing opaque value.
+ *
+ * @param deserializers list of deserializers
+ */
+ public void setRefreshTokenDeserializers(
+ @Nonnull final List<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>> deserializers) {
+ checkSetterPreconditions();
+ refreshTokenDeserializers =
+ Constraint.isNotNull(deserializers, "List of refresh token deserializers cannot be null");
+ }
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -264,50 +283,49 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
final RefreshTokenGrant refreshTokentokenGrant = (RefreshTokenGrant) grant;
if (refreshTokentokenGrant.getRefreshToken() != null
&& refreshTokentokenGrant.getRefreshToken().getValue() != null) {
- try {
- final RefreshTokenClaimsSet refreshTokenClaimsSet = RefreshTokenClaimsSet
- .parse(refreshTokentokenGrant.getRefreshToken().getValue(), dataSealer);
- final String rootJti = refreshTokenClaimsSet.getRootTokenIdentifier();
- final String rootJtiToUse;
- if (StringSupport.trimOrNull(rootJti) == null) {
- log.warn("{} No root token identifier returned, using JWT id for checking revocation status",
+ final RefreshTokenClaimsSet refreshTokenClaimsSet = deserializeRefreshToken(profileRequestContext,
+ refreshTokentokenGrant.getRefreshToken().getValue());
+ if (refreshTokenClaimsSet == null) {
+ log.warn("{} Unwrapping refresh token failed", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+ return;
+ }
+ final String rootJti = refreshTokenClaimsSet.getRootTokenIdentifier();
+ final String rootJtiToUse;
+ if (StringSupport.trimOrNull(rootJti) == null) {
+ log.warn("{} No root token identifier returned, using JWT id for checking revocation status",
+ getLogPrefix());
+ rootJtiToUse = refreshTokenClaimsSet.getID();
+ } else {
+ rootJtiToUse = rootJti;
+ }
+ if (revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootJtiToUse)) {
+ log.error("{} Authz code {} and all derived tokens have been revoked", getLogPrefix(),
+ rootJtiToUse);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+ return;
+ } else if (revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ refreshTokenClaimsSet.getID())) {
+ log.error("{} The refresh token {} has been revoked. Revoking the full chain now.",
+ getLogPrefix(), refreshTokenClaimsSet.getID());
+ if (!revokeChain(rootJtiToUse,
+ chainRevocationLifetimeLookupStrategy.apply(profileRequestContext))) {
+ log.error("{} Fatal error, unable to store revocation into the revocation cache",
getLogPrefix());
- rootJtiToUse = refreshTokenClaimsSet.getID();
- } else {
- rootJtiToUse = rootJti;
- }
- if (revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootJtiToUse)) {
- log.error("{} Authz code {} and all derived tokens have been revoked", getLogPrefix(),
- rootJtiToUse);
- ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
- return;
- } else if (revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
- refreshTokenClaimsSet.getID())) {
- log.error("{} The refresh token {} has been revoked. Revoking the full chain now.",
- getLogPrefix(), refreshTokenClaimsSet.getID());
- if (!revokeChain(rootJtiToUse,
- chainRevocationLifetimeLookupStrategy.apply(profileRequestContext))) {
- log.error("{} Fatal error, unable to store revocation into the revocation cache",
- getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
- return;
- }
- ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
return;
}
- if (refreshTokenClaimsSet.getChainExp() != null
- && refreshTokenClaimsSet.getChainExp().isBefore(Instant.now())) {
- log.warn("{} Refresh token chain has expired on {}", getLogPrefix(),
- refreshTokenClaimsSet.getChainExp());
- ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
- return;
- }
- tokenClaimsSet = refreshTokenClaimsSet;
- } catch (final ParseException | DataSealerException e) {
- log.warn("{} Unwrapping refresh token failed {}", getLogPrefix(), e.getMessage());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
return;
}
+ if (refreshTokenClaimsSet.getChainExp() != null
+ && refreshTokenClaimsSet.getChainExp().isBefore(Instant.now())) {
+ log.warn("{} Refresh token chain has expired on {}", getLogPrefix(),
+ refreshTokenClaimsSet.getChainExp());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+ return;
+ }
+ tokenClaimsSet = refreshTokenClaimsSet;
if (Instant.now().isAfter(tokenClaimsSet.getAuthenticationTime().plus(refreshTokenChainLifetime))) {
log.warn("{} Refresh token chain is expired, the authentication instant was {}", getLogPrefix(),
tokenClaimsSet.getAuthenticationTime());
@@ -340,6 +358,29 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
}
// Checkstyle: CyclomaticComplexity|MethodLength|ReturnCount ON
+ /**
+ * Attempt to deseriaalize a (serialized) refresh token value via configured deserializers.
+ *
+ * @param profileRequestContext The profile request context given to the deserializers
+ * @param refreshToken The serialized refresh token value
+ * @return refresh token claims set, or null if it couldn't be parsed
+ */
+ protected RefreshTokenClaimsSet deserializeRefreshToken(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final String refreshToken) {
+ try {
+ return RefreshTokenClaimsSet.parse(refreshToken, dataSealer);
+ } catch (ParseException | DataSealerException e) {
+ }
+ for (final BiFunction<ProfileRequestContext, String, RefreshTokenClaimsSet> deserializer :
+ refreshTokenDeserializers) {
+ final RefreshTokenClaimsSet deserializedSet = deserializer.apply(profileRequestContext, refreshToken);
+ if (deserializedSet != null) {
+ return deserializedSet;
+ }
+ }
+ return null;
+ }
+
/**
* Revokes the token chain with the given id, optionally with a given lifetime. If the given lifetime is null,
* the default lifetime set to the {@link RevocationCache} is used.
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultJwtRefreshTokenDeserializationFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultJwtRefreshTokenDeserializationFunction.java
new file mode 100644
index 00000000..38761b0b
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultJwtRefreshTokenDeserializationFunction.java
@@ -0,0 +1,220 @@
+/*
+ * Licensed 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.logic;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.CredentialResolver;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.security.criteria.UsageCriterion;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.oidc.security.impl.JWTSignatureValidationUtil;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
+
+/**
+ * Default implementation for deserializing an incoming JWT refresh token into the refresh token claims set. The JWT
+ * needs to be signed in a way that it can be validated using a credential returned by the configurable
+ * {@link CredentialResolver}. The JWT claims are validated via configurable {@link ClaimsValidator}, before the
+ * contents of the 'for_op' claim are decrypted into {@link RefreshTokenClaimsSet}.
+ */
+public class DefaultJwtRefreshTokenDeserializationFunction extends AbstractInitializableComponent
+ implements BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultJwtRefreshTokenDeserializationFunction.class);
+
+ /** The claims validator to use for validating the JWT refresh token. */
+ @NonnullAfterInit private ClaimsValidator claimsValidator;
+
+ /** Source of signing keys used for validating the signature of the JWT refresh token. */
+ @NonnullAfterInit private CredentialResolver credentialResolver;
+
+ /** Data sealer for decrypting the private parts of the refresh token. */
+ @NonnullAfterInit private DataSealer dataSealer;
+
+ /** Strategy used to validate the incoming JWT type header value. */
+ @NonnullAfterInit private BiPredicate<ProfileRequestContext,String> typeHeaderValidationStrategy;
+
+ /**
+ * Constructor.
+ */
+ public DefaultJwtRefreshTokenDeserializationFunction() {
+ }
+
+ /**
+ * Set the data sealer instance to use.
+ *
+ * @param sealer data sealer to use
+ */
+ public void setDataSealer(@Nonnull final DataSealer sealer) {
+ checkSetterPreconditions();
+ dataSealer = Constraint.isNotNull(sealer, "Data sealer cannot be null");
+ }
+
+ /**
+ * Set the claims validator lookup strategy.
+ *
+ * @param validator claims validator
+ */
+ public void setClaimsValidator(@Nonnull final ClaimsValidator validator) {
+ checkSetterPreconditions();
+ claimsValidator = Constraint.isNotNull(validator, "Claims validator cannot be null");
+ }
+
+ /**
+ * Set the source of signing keys to use for JWT signature verification.
+ *
+ * @param resolver signing key resolver
+ */
+ public void setCredentialResolver(@Nullable final CredentialResolver resolver) {
+ checkSetterPreconditions();
+ credentialResolver = Constraint.isNotNull(resolver, "Credential resolver cannot be null");
+ }
+
+ /**
+ * Set the strategy used to validate the incoming JWT type header value.
+ *
+ * @param strategy What to set.
+ */
+ public void setTypeHeaderValidationStrategy(
+ @Nonnull final BiPredicate<ProfileRequestContext,String> strategy) {
+ checkSetterPreconditions();
+ typeHeaderValidationStrategy = Constraint.isNotNull(strategy,
+ "Type header validation strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (dataSealer == null) {
+ throw new ComponentInitializationException("Data sealer cannot be null");
+ }
+ if (claimsValidator == null) {
+ throw new ComponentInitializationException("Claims validator cannot be null");
+ }
+ if (credentialResolver == null) {
+ throw new ComponentInitializationException("Credential resolver cannot be null");
+ }
+ if (typeHeaderValidationStrategy == null) {
+ throw new ComponentInitializationException("Type header validation strategy cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public RefreshTokenClaimsSet apply(@Nullable final ProfileRequestContext profileRequestContext,
+ @Nullable final String token) {
+ if (profileRequestContext == null) {
+ log.error("Profile request context is null");
+ return null;
+ }
+
+ final SignedJWT signedJWT;
+ final JWTClaimsSet claimsSet;
+ try {
+ signedJWT = SignedJWT.parse(token);
+ claimsSet = signedJWT.getJWTClaimsSet();
+ } catch (final ParseException e) {
+ log.debug("Could not parse the JWT out of incoming token {}", token, e);
+ return null;
+ }
+
+ if (claimsSet == null) {
+ log.warn("Could not find any claims inside the incoming JWT token");
+ return null;
+ }
+
+ final JOSEObjectType typ = signedJWT.getHeader().getType();
+ if (!typeHeaderValidationStrategy.test(profileRequestContext, typ == null ? null : typ.getType())) {
+ log.debug("JWT type header {} did not pass validation", typ != null ? typ.getType() : "null");
+ return null;
+ }
+
+ final Collection<Credential> credList = new ArrayList<>();
+ final CriteriaSet criteriaSet = new CriteriaSet(new UsageCriterion(UsageType.SIGNING));
+ try {
+ final Iterable<Credential> creds = credentialResolver.resolve(criteriaSet);
+ creds.forEach(credList::add);
+ } catch (final ResolverException e) {
+ log.error("Failure resolving signing credentials, can't verify JWT signature", e);
+ return null;
+ }
+ final String errorEventId = JWTSignatureValidationUtil.validateSignatureEx(credList, signedJWT,
+ OidcEventIds.INVALID_GRANT);
+ if (errorEventId != null) {
+ log.warn("Signature on refresh token ID '{}' invalid", claimsSet.getJWTID());
+ return null;
+ }
+
+ try {
+ claimsValidator.validate(claimsSet, profileRequestContext);
+ } catch (final JWTValidationException e) {
+ log.warn("JWT refresh token did not pass the claims validation", e);
+ return null;
+ }
+
+ return decryptSealedClaimsSet(claimsSet);
+ }
+
+ /**
+ * Decrypt the sealed claims from 'for_op' claim and build {@link RefreshTokenClaimsSet} out of its contents.
+ *
+ * @param claimsSet The claims set containing the 'for_op' claim.
+ * @return The refresh token claims set, or null if it couldn't be parsed from the claim
+ */
+ protected RefreshTokenClaimsSet decryptSealedClaimsSet(@Nonnull final JWTClaimsSet claimsSet) {
+ try {
+ final String sealedClaimsSet = claimsSet.getStringClaim(TokenClaimsSet.KEY_SEALED_FOR_OP);
+ if (sealedClaimsSet == null) {
+ log.error("Could not find sealed claims set from the JWT claims set");
+ return null;
+ }
+ assert dataSealer != null;
+ return RefreshTokenClaimsSet.parse(sealedClaimsSet, dataSealer);
+ } catch (final ParseException | DataSealerException e) {
+ log.warn("Could not decrypt the sealed claims set", e);
+ }
+ return null;
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultJwtRefreshTokenSerializationFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultJwtRefreshTokenSerializationFunction.java
new file mode 100644
index 00000000..b90b8039
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultJwtRefreshTokenSerializationFunction.java
@@ -0,0 +1,251 @@
+/*
+ * Licensed 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.logic;
+
+import java.util.Date;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.handler.MessageHandler;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.oidc.security.impl.JWSTokenSigner;
+import net.shibboleth.oidc.security.jose.SignatureException;
+import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
+import net.shibboleth.oidc.security.jose.context.SecurityParametersContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
+
+/**
+ * Default implementation for serializing the refresh token claims set into a JWT refresh token.
+ *
+ * The JWT refresh token contains the following claims:
+ * <ul>
+ * <li>iss - The issuer value fetched from {@link RefreshTokenClaimsSet#getIssuer()}</li>
+ * <li>aud - The audience value fetched from {@link #audienceLookupStrategy}</li>
+ * <li>iat - The issued at value fetched from {@link RefreshTokenClaimsSet#getIssuedAt()}</li>
+ * <li>exp - The expiration value fetched from {@link RefreshTokenClaimsSet#getExp()}</li>
+ * <li>jti - The token identifier value fetched from {@link RefreshTokenClaimsSet#getID()}</li>
+ * <li>client_id - The client ID value fetched from {@link RefreshTokenClaimsSet#getClientID()}</li>
+ * <li>for_op - the internal state information for OP's use (the sealed refresh token claims set)</li>
+ * </ul>
+ */
+public class DefaultJwtRefreshTokenSerializationFunction extends AbstractInitializableComponent
+ implements BiFunction<ProfileRequestContext,RefreshTokenClaimsSet,String> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultJwtRefreshTokenSerializationFunction.class);
+
+ /** Data sealer for sealing private parts of the refresh token. */
+ @NonnullAfterInit private DataSealer dataSealer;
+
+ /** Strategy used to look up the {@link SecurityParametersContext} to set the parameters for. */
+ @Nonnull private Function<ProfileRequestContext, SecurityParametersContext> securityParametersContextLookupStrategy;
+
+ /** Handler that resolves and populates {@link SignatureSigningParameters}. */
+ @NonnullAfterInit MessageHandler signingParametersHandler;
+
+ /** Strategy used to lookup the type header value for the JWT. */
+ @NonnullAfterInit private BiFunction<ProfileRequestContext,RefreshTokenClaimsSet,String> typeHeaderLookupStrategy;
+
+ /** Strategy to find the audience value from the context.*/
+ @NonnullAfterInit private Function<ProfileRequestContext, String> audienceLookupStrategy;
+
+ /**
+ * Constructor.
+ */
+ public DefaultJwtRefreshTokenSerializationFunction() {
+ final Function<ProfileRequestContext, SecurityParametersContext> spcls =
+ new ChildContextLookup<>(SecurityParametersContext.class, true).compose(
+ new InboundMessageContextLookup());
+ assert spcls != null;
+ securityParametersContextLookupStrategy = spcls;
+ }
+
+ /**
+ * Set the data sealer instance to use.
+ *
+ * @param sealer What to set.
+ */
+ public void setDataSealer(@Nonnull final DataSealer sealer) {
+ checkSetterPreconditions();
+ dataSealer = Constraint.isNotNull(sealer, "Data sealer cannot be null");
+ }
+
+ /**
+ * Set the strategy used to look up the {@link SecurityParametersContext} to set the parameters for.
+ *
+ * @param strategy What to set.
+ */
+ public void setSecurityParametersContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, SecurityParametersContext> strategy) {
+ checkSetterPreconditions();
+ securityParametersContextLookupStrategy =
+ Constraint.isNotNull(strategy, "The security parameters context lookup strategy cannot be null");
+ }
+ /**
+ * Set the handler that resolves and populates {@link SignatureSigningParameters}.
+ *
+ * @param handler What to set.
+ */
+ public void setSigningParametersHandler(@Nonnull final MessageHandler handler) {
+ checkSetterPreconditions();
+ signingParametersHandler = Constraint.isNotNull(handler, "The signing parameters handler cannot be null");
+ }
+
+ /**
+ * Set the strategy used to lookup the type header value for the JWT.
+ *
+ * @param strategy What to set.
+ */
+ public void setTypeHeaderLookupStrategy(
+ @Nonnull final BiFunction<ProfileRequestContext,RefreshTokenClaimsSet,String> strategy) {
+ checkSetterPreconditions();
+ typeHeaderLookupStrategy = Constraint.isNotNull(strategy, "The type header lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the audience lookup strategy.
+ *
+ * @param strategy the strategy.
+ */
+ public void setAudienceLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, String> strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+
+ audienceLookupStrategy = Constraint.isNotNull(strategy, "Audience lookup strategy can not be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (dataSealer == null) {
+ throw new ComponentInitializationException("The dat sealer cannot be null");
+ }
+ if (signingParametersHandler == null) {
+ throw new ComponentInitializationException("The signing parameters handler cannot be null");
+ }
+ if (typeHeaderLookupStrategy == null) {
+ throw new ComponentInitializationException("The type header lookup strategy cannot be null");
+ }
+ if (audienceLookupStrategy == null) {
+ throw new ComponentInitializationException("The audience lookup strategy cannot be null)");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public String apply(@Nullable final ProfileRequestContext profileRequestContext,
+ @Nullable final RefreshTokenClaimsSet claimsSet) {
+ if (profileRequestContext == null || profileRequestContext.getInboundMessageContext() == null) {
+ log.error("Could not find inbound message context");
+ return null;
+ }
+ if (claimsSet == null) {
+ log.error("Given refresh token claims set was null");
+ return null;
+ }
+ final MessageContext messageContext = profileRequestContext.getInboundMessageContext();
+ assert messageContext != null;
+ try {
+ signingParametersHandler.invoke(messageContext);
+ } catch (final MessageHandlerException e) {
+ log.error("Could not populate signing parameters", e);
+ return null;
+ }
+ final SecurityParametersContext securityContext = securityParametersContextLookupStrategy.apply(profileRequestContext);
+ if (securityContext == null || securityContext.getSignatureSigningParameters() == null) {
+ log.error("Could not find signature signing parameters context after population");
+ return null;
+ }
+ final SignatureSigningParameters signingParameters = securityContext.getSignatureSigningParameters() ;
+ assert signingParameters != null;
+ final String typeHeader = typeHeaderLookupStrategy.apply(profileRequestContext, claimsSet);
+ final String audience = audienceLookupStrategy.apply(profileRequestContext);
+ if (audience == null) {
+ log.error("Could not resolve audience for the JWT");
+ return null;
+ }
+ final SignedJWT jwt = constructJWT(claimsSet, audience, signingParameters, typeHeader);
+ return jwt != null ? jwt.serialize() : null;
+ }
+
+ /**
+ * Construct a {@link SignedJWT} with the given input claims and signing parameters.
+ * @param claimsSet The input for payload
+ * @param signingParameters The signing parameters
+ * @return A signed JWT, or null
+ */
+ @Nullable protected SignedJWT constructJWT(@Nonnull final RefreshTokenClaimsSet claimsSet,
+ @Nonnull final String audience, @Nonnull final SignatureSigningParameters signingParameters,
+ @Nullable final String typeHeader) {
+ final String jti = claimsSet.getID();
+ if (jti == null) {
+ log.error("No ID was set in the claims set");
+ return null;
+ }
+ final ClientID clientId = claimsSet.getClientID();
+ if (clientId == null || clientId.getValue() == null) {
+ log.error("No clientID was set in the claims set");
+ return null;
+ }
+ final JWTClaimsSet jwtClaims;
+ try {
+ assert dataSealer != null;
+ final String sealedClaim = claimsSet.serialize(dataSealer);
+ jwtClaims = new JWTClaimsSet.Builder()
+ .issuer(claimsSet.getIssuer())
+ .audience(audience)
+ .issueTime(Date.from(claimsSet.getIssuedAt()))
+ .expirationTime(Date.from(claimsSet.getExp()))
+ .jwtID(jti)
+ .claim(TokenClaimsSet.KEY_CLIENTID, clientId.getValue())
+ .claim(TokenClaimsSet.KEY_SEALED_FOR_OP, sealedClaim)
+ .build();
+ } catch (final DataSealerException e) {
+ log.error("Could not encrypt the {} claim", TokenClaimsSet.KEY_SEALED_FOR_OP, e);
+ return null;
+ }
+ assert jwtClaims != null;
+ final JWSTokenSigner signer = new JWSTokenSigner(signingParameters);
+ try {
+ return signer.sign(jwtClaims, typeHeader);
+ } catch (final SignatureException e) {
+ log.error("Could not sign the JWT", e);
+ }
+ return null;
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml
index d769dd4c..d1232104 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml
@@ -39,7 +39,8 @@
<bean id="ProcessTokenForIntrospection"
class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ProcessTokenForIntrospection" scope="prototype"
p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
- p:credentialResolver-ref="RelyingPartyCredentialResolver"/>
+ p:credentialResolver-ref="RelyingPartyCredentialResolver"
+ p:refreshTokenDeserializers-ref="shibboleth.oidc.DefaultRefreshTokenDeserializers"/>
<bean id="RelyingPartyCredentialResolver" class="net.shibboleth.profile.relyingparty.RelyingPartyCredentialResolver"
c:_0-ref="shibboleth.RelyingPartyResolverService" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml
index b0c0671c..993e7dcc 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml
@@ -39,7 +39,8 @@
<bean id="ProcessTokenForRevocation"
class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ProcessTokenForRevocation" scope="prototype"
p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
- p:credentialResolver-ref="RelyingPartyCredentialResolver"/>
+ p:credentialResolver-ref="RelyingPartyCredentialResolver"
+ p:refreshTokenDeserializers-ref="shibboleth.oidc.DefaultRefreshTokenDeserializers"/>
<bean id="RelyingPartyCredentialResolver" class="net.shibboleth.profile.relyingparty.RelyingPartyCredentialResolver"
c:_0-ref="shibboleth.RelyingPartyResolverService" />
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 9239117f..7a271bf4 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
@@ -12,4 +12,53 @@
<bean id="InitializeAuthenticationContext"
class="net.shibboleth.idp.saml.profile.impl.InitializeAuthenticationContext" scope="prototype" />
+ <util:list id="shibboleth.oidc.DefaultRefreshTokenDeserializers">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultJwtRefreshTokenDeserializationFunction"
+ scope="prototype"
+ p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}">
+ <property name="credentialResolver">
+ <bean class="net.shibboleth.profile.relyingparty.RelyingPartyCredentialResolver"
+ c:_0-ref="shibboleth.RelyingPartyResolverService" />
+ </property>
+ <property name="typeHeaderValidationStrategy">
+ <bean parent="shibboleth.BiConditions.Expression" c:expression="#input2 == null"/>
+ </property>
+ <property name="claimsValidator">
+ <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator">
+ <property name="claimValidators">
+ <util:list value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+ <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+ p:requiredClaims="jti" />
+ <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
+ p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+ <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+ p:claimName="iss">
+ <property name="valueToMatchLookupStrategy">
+ <bean class="net.shibboleth.shared.logic.BiFunctionSupport"
+ factory-method="forFunctionOfFirstArg"
+ c:_0-ref="shibboleth.ResponderIdLookup.Simple" />
+ </property>
+ </bean>
+ <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+ p:claimName="client_id">
+ <property name="valueToMatchLookupStrategy">
+ <bean class="net.shibboleth.shared.logic.BiFunctionSupport"
+ factory-method="forFunctionOfFirstArg"
+ c:_0-ref="shibboleth.RelyingPartyIdLookup.Simple" />
+ </property>
+ </bean>
+ <bean class="net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator">
+ <property name="audienceLookupStrategy">
+ <bean parent="shibboleth.BiFunctions.Expression"
+ c:expression="#custom.get().getRequestURL().toString().replace('/profile/oauth2/introspection','/profile/oidc/token').replace('/profile/oauth2/revocation','/profile/oidc/token')"
+ p:customObject-ref="shibboleth.HttpServletRequestSupplier" />
+ </property>
+ </bean>
+ </util:list>
+ </property>
+ </bean>
+ </property>
+ </bean>
+ </util:list>
+
</beans>
\ No newline at end of file
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 8ea453c1..056d204b 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
@@ -71,13 +71,14 @@
<bean id="ValidateGrant" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateGrant" scope="prototype"
c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
p:replayCache-ref="shibboleth.ReplayCache"
- p:revocationCache-ref="shibboleth.oidc.RevocationCache">
+ p:revocationCache-ref="shibboleth.oidc.RevocationCache"
+ p:refreshTokenDeserializers-ref="#{'%{idp.oauth2.refreshToken.deserializers:shibboleth.oidc.DefaultRefreshTokenDeserializers}'.trim()}">
<property name="chainRevocationLifetimeLookupStrategy">
<bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultChainRevocationLifetimeLookupStrategy"
p:clockSkew="%{idp.policy.clockSkew:PT5M}" p:useActiveProfileOnly="false" />
</property>
</bean>
-
+
<bean id="ValidatePKCE" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidatePKCE" scope="prototype" />
<bean id="ValidateRedirectURI" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateRedirectURI"
@@ -404,14 +405,63 @@
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetRefreshTokenToResponseContext" scope="prototype"
c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
p:revocationCache-ref="shibboleth.oidc.RevocationCache"
- p:activationCondition-ref="#{'%{idp.oauth2.refreshToken.activation:DefaultRefreshTokenActivationCondition}'.trim()}">
+ p:activationCondition-ref="#{'%{idp.oauth2.refreshToken.activation:DefaultRefreshTokenActivationCondition}'.trim()}"
+ p:refreshTokenSerializationStrategies-ref="#{'%{idp.oauth2.refreshToken.serializationStrategies:shibboleth.oidc.DefaultRefreshTokenSerializationStrategies}'.trim()}">
<property name="tokenRevocationLifetimeLookupStrategy">
<bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultTokenRevocationLifetimeLookupStrategy"
p:clockSkew="%{idp.policy.clockSkew:PT5M}" />
</property>
+ <property name="identifierGeneratorLookupStrategy">
+ <bean class="net.shibboleth.profile.config.navigate.IdentifierGenerationStrategyLookupFunction"
+ p:defaultIdentifierGenerationStrategy-ref="shibboleth.DefaultIdentifierGenerationStrategy" />
+ </property>
</bean>
+ <util:map id="shibboleth.oidc.DefaultRefreshTokenSerializationStrategies" scope="prototype">
+ <entry key="JWT">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultJwtRefreshTokenSerializationFunction"
+ scope="prototype"
+ p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}">
+ <property name="signingParametersHandler">
+ <bean class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParametersHandler"
+ scope="prototype">
+ <property name="configurationLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose">
+ <constructor-arg name="f">
+ <bean class="org.opensaml.messaging.context.navigate.ParentContextLookup"
+ c:type="org.opensaml.profile.context.ProfileRequestContext" />
+ </constructor-arg>
+ <constructor-arg name="g">
+ <bean lazy-init="true"
+ class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction" />
+ </constructor-arg>
+ </bean>
+ </property>
+ <property name="signatureSigningParametersResolver">
+ <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationSignatureSigningParametersResolver">
+ <constructor-arg name="signatureAlgorithmLookupStrategy">
+ <bean class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+ c:keyName="id_token_signed_response_alg" />
+ </constructor-arg>
+ <constructor-arg name="defaultAlgorithmValue" value="RS256" />
+ </bean>
+ </property>
+ </bean>
+ </property>
+ <property name="audienceLookupStrategy">
+ <bean parent="shibboleth.Functions.Expression"
+ c:expression="#custom.get().getRequestURL().toString()"
+ p:customObject-ref="shibboleth.HttpServletRequestSupplier" />
+ </property>
+ <property name="typeHeaderLookupStrategy">
+ <bean parent="shibboleth.BiFunctions.Expression" c:expression="#null" />
+ </property>
+ </bean>
+ </entry>
+ </util:map>
+
+
<bean id="DefaultRefreshTokenActivationCondition" parent="shibboleth.Conditions.AND">
<constructor-arg>
<list>
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcApiFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcApiFlowTest.java
index ade67587..c0336e11 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcApiFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcApiFlowTest.java
@@ -22,6 +22,7 @@ import java.security.interfaces.ECPrivateKey;
import java.time.Instant;
import java.util.Collection;
import java.util.Date;
+import java.util.List;
import com.nimbusds.jose.Algorithm;
import com.nimbusds.jose.JOSEException;
@@ -31,6 +32,7 @@ import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.ECDSASigner;
import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.PlainJWT;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.Scope;
@@ -41,6 +43,7 @@ import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.security.DataSealerException;
@@ -89,6 +92,29 @@ public class AbstractOidcApiFlowTest extends AbstractOidcFlowTest {
return new RefreshToken(builder.build().serialize(getDataSealer()));
}
+ protected String buildJwtRefreshToken(final String clientId, final String id, final String rootId,
+ final Scope scope, final List<String> aud, final Instant exp, final String type,
+ final String... consentedClaims)
+ throws NoSuchAlgorithmException, URISyntaxException, DataSealerException,
+ ComponentInitializationException, JOSEException {
+ final String sealedForOp =
+ buildRefreshToken(clientId, "mockSub", scope, null, id, rootId, Instant.now(), exp).getValue();
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+ .issuer("https://op.example.org")
+ .audience(aud)
+ .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+ .jwtID(idGenerator.generateIdentifier())
+ .claim(TokenClaimsSet.KEY_CLIENTID, clientId)
+ .claim(TokenClaimsSet.KEY_SEALED_FOR_OP, sealedForOp)
+ .build();
+ final RSASSASigner signer = new RSASSASigner(loadRSSigningCredential().getPrivateKey());
+ final SignedJWT jwt = new SignedJWT(
+ new JWSHeader.Builder(JWSAlgorithm.RS256).type(type == null ? null : new JOSEObjectType(type)).build(),
+ claimsSet);
+ jwt.sign(signer);
+ return jwt.serialize();
+ }
+
protected BearerAccessToken buildLegacyToken(final String clientId, final String subject, final Scope scope,
final ClaimsSet userInfoDeliverySet, final String... consentedClaims)
throws URISyntaxException, NoSuchAlgorithmException, DataSealerException, ComponentInitializationException {
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 76874fee..9ff6545e 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
@@ -230,6 +230,47 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
Assert.assertNull(resp.getAudience());
}
+ @Test
+ public void testSuccessWithJwtRefreshToken() throws IOException, NoSuchAlgorithmException, URISyntaxException,
+ DataSealerException, ComponentInitializationException, JOSEException {
+ final String rootId = idGenerator.generateIdentifier();
+ final String jti = idGenerator.generateIdentifier();
+ storeMetadata(storageService, clientId, clientSecret, scope);
+ setBasicAuth(clientId, clientSecret);
+ setHttpFormRequest("POST", Map.of(
+ "token",
+ buildJwtRefreshToken(clientId, jti, rootId, Scope.parse("openid"),
+ List.of("http://localhost/idp/profile/oidc/token"), Instant.now().plusSeconds(600), null),
+ "token_type",
+ "refresh_token"));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final TokenIntrospectionSuccessResponse resp =
+ parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
+ Assert.assertTrue(resp.isActive());
+ Assert.assertEquals(resp.getClientID().getValue(), clientId);
+ Assert.assertEquals(resp.getScope(), Scope.parse("openid"));
+ Assert.assertNull(resp.getAudience());
+ }
+
+ @Test
+ public void testFailureWithJwtRefreshTokenWrongType() throws IOException, NoSuchAlgorithmException,
+ URISyntaxException, DataSealerException, ComponentInitializationException, JOSEException {
+ final String rootId = idGenerator.generateIdentifier();
+ final String jti = idGenerator.generateIdentifier();
+ storeMetadata(storageService, clientId, clientSecret, scope);
+ setBasicAuth(clientId, clientSecret);
+ setHttpFormRequest("POST", Map.of(
+ "token",
+ buildJwtRefreshToken(clientId, jti, rootId, Scope.parse("openid"),
+ List.of("http://localhost/idp/profile/oidc/token"), Instant.now().plusSeconds(600), "rt+jwt"),
+ "token_type",
+ "refresh_token"));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final TokenIntrospectionSuccessResponse resp =
+ parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
+ Assert.assertFalse(resp.isActive());
+ }
+
@Test
public void testFailureWithChainExpiredRefreshToken() throws IOException, NoSuchAlgorithmException,
URISyntaxException, DataSealerException, ComponentInitializationException {
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 2f70d031..1a9c8115 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
@@ -183,6 +183,38 @@ public class RevocationFlowTest extends AbstractOidcClientAuthenticationFlowTest
Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
}
+ @Test
+ public void testSuccessSingleJwtRefreshToken() throws IOException, NoSuchAlgorithmException, URISyntaxException,
+ DataSealerException, ComponentInitializationException, JOSEException {
+ final String id = idGenerator.generateIdentifier();
+ final String rootId = idGenerator.generateIdentifier();
+ setBasicAuth(clientIdSingle, clientSecret);
+ storeMetadata(storageService, clientIdSingle, clientSecret, scope);
+ setHttpFormRequest("POST", Collections.singletonMap("token", buildJwtRefreshToken(clientIdSingle, id,
+ rootId, Scope.parse("openid"), List.of("http://localhost/idp/profile/oidc/token"),
+ Instant.now().plusSeconds(600), null)));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ parseSuccessResponse(result, OAuth2RevocationSuccessResponse.class);
+ Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, id));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
+ }
+
+ @Test
+ public void testFailedSingleJwtRefreshTokenWrongType() throws IOException, NoSuchAlgorithmException,
+ URISyntaxException, DataSealerException, ComponentInitializationException, JOSEException {
+ final String id = idGenerator.generateIdentifier();
+ final String rootId = idGenerator.generateIdentifier();
+ setBasicAuth(clientIdSingle, clientSecret);
+ storeMetadata(storageService, clientIdSingle, clientSecret, scope);
+ setHttpFormRequest("POST", Collections.singletonMap("token", buildJwtRefreshToken(clientIdSingle, id,
+ rootId, Scope.parse("openid"), List.of("http://localhost/idp/profile/oidc/token"),
+ Instant.now().plusSeconds(600), "rt+jwt")));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ parseSuccessResponse(result, OAuth2RevocationSuccessResponse.class);
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, id));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
+ }
+
@Test
public void testSuccessSingleChainExpiredRefreshToken_notRevoked() throws IOException, NoSuchAlgorithmException,
URISyntaxException, DataSealerException, ComponentInitializationException {
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 b71cf236..224048e0 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
@@ -43,7 +43,6 @@ import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JWEAlgorithm;
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;
@@ -93,6 +92,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
String clientIdRefreshTokenRotation = "mockClientIdRefreshTokenRotation";
String clientIdJwtAccessToken = "mockClientIdJwtAccessToken";
String clientIdNoIdTokenViaRefreshToken = "mockClientIdNoIdTokenViaRefreshToken";
+ String clientIdJwtRefreshToken = "mockClientIdNotMDDrivenRefreshTokenJwt";
String codeVerifier = "9234567812345678123456781234567812345678123456781234567812345678";
String resourceUri = "https://rp.example.org";
@@ -124,6 +124,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
removeMetadata(storageService, clientIdNoIdTokenViaRefreshToken);
removeMetadata(storageService, resourceUri);
removeMetadata(storageService, resourceNonUri);
+ removeMetadata(storageService, clientIdNoIdTokenViaRefreshToken);
}
@Test
@@ -447,6 +448,24 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
Assert.assertEquals(getSidFromRefreshToken(response.getTokens().getRefreshToken()), sid);
Assert.assertEquals(getSidFromJWT(response.getOIDCTokens().getIDToken()), sid);
}
+
+ @Test
+ public void testValidGrantWithSidRefreshTokenJWT() throws Exception {
+ final String sid = idGenerator.generateIdentifier();
+ final String clientId = clientIdJwtRefreshToken;
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "authorization_code",
+ buildAuthorizationCodeWithSid(clientId, sid), clientId));
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getAccessToken());
+ Assert.assertNotNull(response.getTokens().getRefreshToken());
+ Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+ Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
+ Assert.assertEquals(getSidFromAccessToken(response.getTokens().getAccessToken()), sid);
+ Assert.assertEquals(getSidFromRefreshTokenJWT(response.getTokens().getRefreshToken()), sid);
+ }
+
@Test
public void testValidGrantNonMatchingRedirectURI() throws Exception {
initializeGrantAndRequest(clientId, createRequestParameters(redirectUri + "wrong", "authorization_code",
@@ -756,22 +775,22 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
protected TokenClaimsSet buildDefaultClaimsSet(final String clientId, final Collection<String> aud,
final Instant authTime) throws Exception {
- final Instant now = Instant.now();
- final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
- builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
- .setClientID(new ClientID(clientId))
- .setIssuer(issuer)
- .setPrincipal("jdoe")
- .setSubject("mock")
- .setIssuedAt(now)
- .setExpiresAt(now.plusSeconds(100))
- .setAuthenticationTime(authTime)
- .setRedirectURI(new URI(redirectUri))
- .setAudience(aud)
- .setScope(scope == null ? new Scope() : scope);
- return builder.build();
- }
-
+ final Instant now = Instant.now();
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer(issuer)
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(now)
+ .setExpiresAt(now.plusSeconds(100))
+ .setAuthenticationTime(authTime)
+ .setRedirectURI(new URI(redirectUri))
+ .setAudience(aud)
+ .setScope(scope == null ? new Scope() : scope);
+ return builder.build();
+ }
+
protected String buildRefreshToken(final String clientId, final String id, final String rootId,
final Collection<String> aud, final String... consentedClaims) throws Exception {
final TokenClaimsSet acClaims = buildDefaultClaimsSet(clientId, aud);
@@ -1055,6 +1074,39 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
}
+ @Test
+ public void testValidJwtRefreshTokenGrant() throws Exception {
+ final String id = idGenerator.generateIdentifier();
+ final String rootId = idGenerator.generateIdentifier();
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "refresh_token",
+ buildJwtRefreshToken(clientId, id, rootId, scope, List.of("http://localhost/idp/profile/oidc/token"),
+ Instant.now().plusSeconds(600), null),
+ clientId));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+ final AccessToken accessToken = response.getTokens().getAccessToken();
+ Assert.assertNotNull(accessToken);
+ Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+ Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
+ Assert.assertNull(getSidFromAccessToken(accessToken));
+ Assert.assertNull(getSidFromJWT(response.getOIDCTokens().getIDToken()));
+ validateConsentFromAccessToken(response, false);
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, id));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
+ }
+
+ @Test
+ public void testInvalidJwtRefreshTokenGrantUnsupportedType() throws Exception {
+ final String id = idGenerator.generateIdentifier();
+ final String rootId = idGenerator.generateIdentifier();
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "refresh_token",
+ buildJwtRefreshToken(clientId, id, rootId, scope, List.of("http://localhost/idp/profile/oidc/token"),
+ Instant.now().plusSeconds(600), "rt+jwt"),
+ clientId));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_GRANT_CODE);
+ }
+
@Test
public void testChainExpiredRefreshTokenGrant() throws Exception {
final String id = idGenerator.generateIdentifier();
@@ -1411,4 +1463,26 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
}
}
+ protected String getSidFromRefreshTokenJWT(final RefreshToken refreshToken) {
+ assertRefreshTokenJWT(refreshToken);
+ final RefreshTokenClaimsSet claims;
+ try {
+ final SignedJWT signedJwt = SignedJWT.parse(refreshToken.getValue());
+ claims = RefreshTokenClaimsSet.parse(signedJwt.getJWTClaimsSet()
+ .getStringClaim(TokenClaimsSet.KEY_SEALED_FOR_OP), getDataSealer());
+ return claims.getSessionIdentifier();
+ } catch (ParseException | DataSealerException e) {
+ return null;
+ }
+ }
+
+ protected void assertRefreshTokenJWT(final RefreshToken refreshToken) {
+ Assert.assertNotNull(refreshToken.getValue());
+ try {
+ Assert.assertNotNull(SignedJWT.parse(refreshToken.getValue()));
+ } catch (final ParseException e) {
+ Assert.fail();
+ }
+ }
+
}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContextTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContextTest.java
index ae6bc171..fcb2dd60 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContextTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContextTest.java
@@ -39,6 +39,7 @@ import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
+import org.mockito.Mockito;
import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.storage.impl.MemoryStorageService;
@@ -47,6 +48,8 @@ import org.springframework.webflow.execution.Event;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.openid.connect.sdk.OIDCScopeValue;
@@ -60,6 +63,13 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
StorageServiceRevocationCache revocationCache;
private boolean enforceRotation;
+
+ @SuppressWarnings("unchecked")
+ private Function<ProfileRequestContext,String> refreshTokenTypeLookupStrategy = Mockito.mock(Function.class);
+
+ @SuppressWarnings("unchecked")
+ private Map<String, BiFunction<ProfileRequestContext,RefreshTokenClaimsSet,String>> serializers =
+ Mockito.mock(Map.class);
@BeforeMethod
private void init() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException {
@@ -105,6 +115,8 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
action.setTokenRevocationLifetimeLookupStrategy(revocationLifetimeLookup);
action.setEnforceRefreshTokenRotationCondition(prc -> enforceRotation);
+ action.setRefreshTokenTypeLookupStrategy(refreshTokenTypeLookupStrategy);
+ action.setRefreshTokenSerializationStrategies(serializers);
action.initialize();
return action;
}
@@ -132,6 +144,39 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
Assert.assertEquals(rt.getRootTokenIdentifier(), jit);
}
+ @Test
+ public void testSuccessViaCodeCustomSerializer() throws ComponentInitializationException, NoSuchAlgorithmException,
+ URISyntaxException, ParseException, DataSealerException {
+ final String jit = respCtx.getAuthorizationGrantClaimsSet().getID();
+ Mockito.when(refreshTokenTypeLookupStrategy.apply(Mockito.any())).thenReturn("customType");
+ Mockito.when(serializers.get(Mockito.eq("customType"))).thenReturn(mockSerializer());
+ final Event event = action.execute(requestCtx);
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertNotNull(respCtx.getRefreshToken());
+ final RefreshTokenClaimsSet rt =
+ RefreshTokenClaimsSet.parse(respCtx.getRefreshToken().getValue(), getDataSealer());
+ Assert.assertNotNull(rt);
+ Assert.assertNotEquals(rt.getID(), jit);
+ Assert.assertEquals(rt.getRootTokenIdentifier(), jit);
+ Assert.assertEquals(rt.getClaimsSet().getBooleanClaim("customFlag"), true);
+ }
+
+ protected BiFunction<ProfileRequestContext,RefreshTokenClaimsSet,String> mockSerializer() {
+ return (prc, claimsSet) -> {
+ try {
+ final Map<String,Object> jwtClaimsSet = claimsSet.getClaimsSet().toJSONObject();
+ jwtClaimsSet.put("customFlag", true);
+ claimsSet.setClaimsSet(JWTClaimsSet.parse(jwtClaimsSet));
+ return claimsSet.serialize(getDataSealer());
+ } catch (NoSuchAlgorithmException | DataSealerException | ComponentInitializationException
+ | ParseException e) {
+ Assert.fail(e.getMessage());
+ }
+ return null;
+ };
+
+ }
+
@Test
public void testSuccessViaCodeWithCustomClaim() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
ParseException, DataSealerException {
@@ -181,6 +226,41 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jit));
}
+ @Test
+ public void testSuccessViaRefreshCustomSerializer() throws ComponentInitializationException,
+ NoSuchAlgorithmException, URISyntaxException, ParseException, DataSealerException {
+ final String rootTokenId = new SecureRandomIdentifierGenerationStrategy().generateIdentifier();
+ final TokenClaimsSet claims = new RefreshTokenClaimsSet.Builder()
+ .setJWTID(idGenerator)
+ .setClientID(new ClientID())
+ .setIssuer("issuer")
+ .setPrincipal("userPrin")
+ .setSubject("subject")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now())
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI("http://example.com"))
+ .setScope(new Scope())
+ .setACR(new ACR("0"))
+ .setRootTokenIdentifier(rootTokenId)
+ .build();
+ final String jit = claims.getID();
+ respCtx.setAuthorizationGrantClaimsSet(claims);
+ Mockito.when(refreshTokenTypeLookupStrategy.apply(Mockito.any())).thenReturn("customType");
+ Mockito.when(serializers.get(Mockito.eq("customType"))).thenReturn(mockSerializer());
+ final Event event = action.execute(requestCtx);
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertNotNull(respCtx.getRefreshToken());
+ final RefreshTokenClaimsSet rt =
+ RefreshTokenClaimsSet.parse(respCtx.getRefreshToken().getValue(), getDataSealer());
+ Assert.assertNotNull(rt);
+ Assert.assertNotEquals(rt.getID(), jit);
+ Assert.assertEquals(rt.getRootTokenIdentifier(), rootTokenId);
+ Assert.assertTrue(rt.getChainExp().isAfter(Instant.now()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jit));
+ Assert.assertEquals(rt.getClaimsSet().getBooleanClaim("customFlag"), true);
+ }
+
@Test
public void testSuccessViaRefresh_existingChainExp() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
ParseException, DataSealerException {
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java
index 587e895f..4539ebe5 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java
@@ -25,13 +25,17 @@ import net.shibboleth.idp.profile.testing.ActionTestingSupport;
import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.security.DataSealerException;
import net.shibboleth.shared.security.impl.SecureRandomIdentifierGenerationStrategy;
import java.net.URI;
import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
+import java.util.List;
+import java.util.function.BiFunction;
import java.util.function.Function;
import org.mockito.Mockito;
@@ -93,17 +97,19 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
}
private void init(final Instant authenticationTime) throws Exception {
- init(true, new MockRevocationCache(false, true), null, authenticationTime);
+ init(true, new MockRevocationCache(false, true), null, authenticationTime, null);
}
private void init(boolean refreshTokensEnabled, final RevocationCache revocationCache,
final Function<ProfileRequestContext, Duration> revocationLifetimeLookup) throws Exception {
- init(refreshTokensEnabled, revocationCache, revocationLifetimeLookup, Instant.now());
+ init(refreshTokensEnabled, revocationCache, revocationLifetimeLookup, Instant.now(), null);
}
private void init(boolean refreshTokensEnabled, final RevocationCache revocationCache,
final Function<ProfileRequestContext, Duration> revocationLifetimeLookup,
- final Instant authenticationTime) throws Exception {
+ final Instant authenticationTime,
+ final List<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>> deserializers)
+ throws Exception {
final Instant now = Instant.now();
rootTokenId = "mockId" + now.toEpochMilli();
acClaims = new AuthorizeCodeClaimsSet.Builder()
@@ -135,6 +141,9 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
action.setChainRevocationLifetimeLookupStrategy(revocationLifetimeLookup);
}
action.setRevocationCache(revocationCache);
+ if (deserializers != null) {
+ action.setRefreshTokenDeserializers(deserializers);
+ }
final StorageServiceReplayCache replayCache = new StorageServiceReplayCache();
replayCache.setStorage(storageService);
action.setReplayCache(replayCache);
@@ -243,6 +252,42 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
Assert.assertNotNull(arc.getAuthorizationGrantClaimsSet());
}
+ @Test
+ public void testCustomRefreshTokenNoDeserializers() throws Exception {
+ init();
+ final RefreshToken customToken = new RefreshToken("customPrefix" + rfGrant.getRefreshToken().getValue());
+ final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), new RefreshTokenGrant(customToken));
+ profileRequestCtx.getInboundMessageContext().setMessage(req);
+ ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
+ }
+
+ @Test
+ public void testCustomRefreshTokenSuccess() throws Exception {
+ final List<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>> deserializers =
+ List.of(new BiFunction<>() {
+
+ @Override
+ public RefreshTokenClaimsSet apply(final ProfileRequestContext prc, final String value ) {
+ try {
+ return RefreshTokenClaimsSet.parse(value.substring("customPrefix".length()), getDataSealer());
+ } catch (NoSuchAlgorithmException | ParseException | DataSealerException
+ | ComponentInitializationException e) {
+ Assert.fail("Could not decrypt the custom refresh token", e);
+ }
+ return null;
+ }
+
+ });
+ init(true, new MockRevocationCache(false, true), null, Instant.now(), deserializers);
+ final RefreshToken customToken = new RefreshToken("customPrefix" + rfGrant.getRefreshToken().getValue());
+ final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), new RefreshTokenGrant(customToken));
+ profileRequestCtx.getInboundMessageContext().setMessage(req);
+ ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+ final OIDCAuthenticationResponseContext arc =
+ profileRequestCtx.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
+ Assert.assertNotNull(arc.getAuthorizationGrantClaimsSet());
+ }
+
@Test
public void testRefreshTokenChainExpired() throws Exception {
init(Instant.now().minus(Duration.ofHours(2)));
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultJwtRefreshTokenDeserializationFunctionTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultJwtRefreshTokenDeserializationFunctionTest.java
new file mode 100644
index 00000000..0fbd4c15
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultJwtRefreshTokenDeserializationFunctionTest.java
@@ -0,0 +1,205 @@
+/*
+ * Licensed 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.logic;
+
+import java.security.NoSuchAlgorithmException;
+import java.time.Instant;
+import java.util.List;
+import java.util.function.BiPredicate;
+
+import javax.annotation.Nonnull;
+
+import org.mockito.Mockito;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.CredentialResolver;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
+import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.oidc.security.credential.DefaultClientSecretCredential;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
+
+/**
+ * Unit tests for {@link DefaultJwtRefreshTokenDeserializationFunction}.
+ */
+public class DefaultJwtRefreshTokenDeserializationFunctionTest {
+
+ @Nonnull final String clientSecret = "mockSecret1234567890mockSecret1234567890mockSecret1234567890";
+ DefaultJwtRefreshTokenDeserializationFunction function;
+ ClaimsValidator claimsValidator = Mockito.mock(ClaimsValidator.class);
+ CredentialResolver credentialResolver = Mockito.mock(CredentialResolver.class);
+ @SuppressWarnings("unchecked")
+ BiPredicate<ProfileRequestContext,String> typeHeaderValidationStrategy = Mockito.mock(BiPredicate.class);
+ DataSealer dataSealer;
+
+ @BeforeClass
+ protected void initSealer() {
+ try {
+ dataSealer = BaseOIDCResponseActionTest.initializeDataSealer();
+ } catch (final ComponentInitializationException | NoSuchAlgorithmException e) {
+ Assert.fail("Could not initialize the data sealer", e);
+ }
+ }
+
+ @BeforeMethod
+ protected void initFunction() {
+ try {
+ function = new DefaultJwtRefreshTokenDeserializationFunction();
+ assert dataSealer != null;
+ function.setDataSealer(dataSealer);
+ assert claimsValidator != null;
+ function.setClaimsValidator(claimsValidator);
+ function.setCredentialResolver(credentialResolver);
+ assert typeHeaderValidationStrategy != null;
+ function.setTypeHeaderValidationStrategy(typeHeaderValidationStrategy);
+ function.initialize();
+ } catch (final ComponentInitializationException e) {
+ Assert.fail("Could not initialize the function", e);
+ }
+ }
+
+ @Test
+ public void testNullPrc() {
+ Assert.assertNull(function.apply(null, "mockToken"));
+ }
+
+ @Test
+ public void testNonJwtToken() {
+ Assert.assertNull(function.apply(new ProfileRequestContext(), "mockToken"));
+ }
+
+ @Test
+ public void testPlainJwtToken() throws DataSealerException {
+ final PlainJWT jwt = new PlainJWT(mockJwtClaimsSet());
+ Assert.assertNull(function.apply(new ProfileRequestContext(), jwt.serialize()));
+ }
+
+ @Test
+ public void testSignedJwtNullTypeNotAllowed() throws ResolverException {
+ Mockito.when(typeHeaderValidationStrategy.test(Mockito.any(), Mockito.isNull())).thenReturn(false);
+ final SignedJWT jwt = mockSignedJwt(null);
+ Assert.assertNull(function.apply(new ProfileRequestContext(), jwt.serialize()));
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testSignedJwtNullTypeAllowed() throws ResolverException, JWTValidationException {
+ Mockito.when(typeHeaderValidationStrategy.test(Mockito.any(), Mockito.isNull())).thenReturn(true);
+ Mockito.when(credentialResolver.resolve(Mockito.any())).thenReturn(List.of(clientSecretCredential()));
+ Mockito.doNothing().when(claimsValidator).validate(Mockito.any(), Mockito.any());
+ final SignedJWT jwt = mockSignedJwt(null);
+ Assert.assertNotNull(function.apply(new ProfileRequestContext(), jwt.serialize()));
+ }
+
+ @Test
+ public void testSignedJwtTypeNotAllowed() throws ResolverException {
+ Mockito.when(typeHeaderValidationStrategy.test(Mockito.any(), Mockito.eq("rt+jwt"))).thenReturn(false);
+ final SignedJWT jwt = mockSignedJwt("rt+jwt");
+ Assert.assertNull(function.apply(new ProfileRequestContext(), jwt.serialize()));
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testSignedJwtTypeAllowed() throws ResolverException, JWTValidationException {
+ Mockito.when(typeHeaderValidationStrategy.test(Mockito.any(), Mockito.eq("rt+jwt"))).thenReturn(true);
+ Mockito.when(credentialResolver.resolve(Mockito.any())).thenReturn(List.of(clientSecretCredential()));
+ Mockito.doNothing().when(claimsValidator).validate(Mockito.any(), Mockito.any());
+ final SignedJWT jwt = mockSignedJwt("rt+jwt");
+ Assert.assertNotNull(function.apply(new ProfileRequestContext(), jwt.serialize()));
+ }
+
+ @Test
+ public void testSignedJwtInvalidSignature() throws ResolverException {
+ Mockito.when(typeHeaderValidationStrategy.test(Mockito.any(), Mockito.eq("rt+jwt"))).thenReturn(true);
+ Mockito.when(credentialResolver.resolve(Mockito.any())).thenReturn(List.of(wrongSecretCredential()));
+ final SignedJWT jwt = mockSignedJwt("rt+jwt");
+ Assert.assertNull(function.apply(new ProfileRequestContext(), jwt.serialize()));
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testSignedJwtClaimsValidationFails() throws ResolverException, JWTValidationException {
+ Mockito.when(typeHeaderValidationStrategy.test(Mockito.any(), Mockito.eq("rt+jwt"))).thenReturn(true);
+ Mockito.when(credentialResolver.resolve(Mockito.any())).thenReturn(List.of(clientSecretCredential()));
+ Mockito.doThrow(JWTValidationException.class).when(claimsValidator).validate(Mockito.any(), Mockito.any());
+ final SignedJWT jwt = mockSignedJwt("rt+jwt");
+ Assert.assertNull(function.apply(new ProfileRequestContext(), jwt.serialize()));
+ }
+
+ @SuppressWarnings("null")
+ protected JWTClaimsSet mockJwtClaimsSet() throws DataSealerException {
+ return new JWTClaimsSet.Builder()
+ .claim(TokenClaimsSet.KEY_SEALED_FOR_OP, new RefreshTokenClaimsSet.Builder()
+ .setJWTID("mockId")
+ .setClientID(new ClientID("mockClientId"))
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(600))
+ .setIssuer("mockIssuer")
+ .setAuthenticationTime(Instant.now())
+ .setSubject("mockSubject")
+ .setScope(Scope.parse(""))
+ .build()
+ .serialize(dataSealer))
+ .build();
+ }
+
+ protected SignedJWT mockSignedJwt(final String type) {
+ try {
+ final JWSSigner signer =
+ new MACSigner(clientSecret.getBytes());
+ final SignedJWT signedJwt = new SignedJWT(
+ new JWSHeader.Builder(JWSAlgorithm.HS256)
+ .type(type == null ? null : new JOSEObjectType(type))
+ .build(),
+ mockJwtClaimsSet());
+ signedJwt.sign(signer);
+ return signedJwt;
+ } catch (final JOSEException | DataSealerException e) {
+ Assert.fail("Could not build a signed JWT", e);
+ }
+ return null;
+ }
+
+ protected Credential clientSecretCredential() {
+ return new DefaultClientSecretCredential(clientSecret).toSigningCredential();
+ }
+
+ protected Credential wrongSecretCredential() {
+ return new DefaultClientSecretCredential(clientSecret + "2").toSigningCredential();
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultJwtRefreshTokenSerializationFunctionTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultJwtRefreshTokenSerializationFunctionTest.java
new file mode 100644
index 00000000..26d77a5b
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultJwtRefreshTokenSerializationFunctionTest.java
@@ -0,0 +1,205 @@
+/*
+ * Licensed 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.logic;
+
+import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.mockito.Mockito;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandler;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.security.credential.Credential;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
+import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.oidc.security.credential.DefaultClientSecretCredential;
+import net.shibboleth.oidc.security.impl.JWTSignatureValidationUtil;
+import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
+import net.shibboleth.oidc.security.jose.context.SecurityParametersContext;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.security.DataSealer;
+
+/**
+ * Unit tests for {@link DefaultJwtRefreshTokenSerializationFunction}.
+ */
+public class DefaultJwtRefreshTokenSerializationFunctionTest {
+
+ @Nonnull final String clientSecret = "mockSecret1234567890mockSecret1234567890mockSecret1234567890";
+ DefaultJwtRefreshTokenSerializationFunction function;
+ @SuppressWarnings("unchecked")
+ Function<ProfileRequestContext, SecurityParametersContext> securityParametersContextLookupStrategy =
+ Mockito.mock(Function.class);
+ MessageHandler signingParametersHandler = Mockito.mock(MessageHandler.class);
+ @SuppressWarnings("unchecked")
+ BiFunction<ProfileRequestContext,RefreshTokenClaimsSet,String> typeHeaderLookupStrategy =
+ Mockito.mock(BiFunction.class);
+ @SuppressWarnings("unchecked")
+ Function<ProfileRequestContext, String> audienceLookupStrategy = Mockito.mock(Function.class);
+ DataSealer dataSealer;
+
+ @BeforeClass
+ protected void initSealer() {
+ try {
+ dataSealer = BaseOIDCResponseActionTest.initializeDataSealer();
+ } catch (final ComponentInitializationException | NoSuchAlgorithmException e) {
+ Assert.fail("Could not initialize the data sealer", e);
+ }
+ }
+
+ @BeforeMethod
+ protected void initFunction() {
+ try {
+ function = new DefaultJwtRefreshTokenSerializationFunction();
+ assert dataSealer != null;
+ function.setDataSealer(dataSealer);
+ assert securityParametersContextLookupStrategy != null;
+ function.setSecurityParametersContextLookupStrategy(securityParametersContextLookupStrategy);
+ assert signingParametersHandler != null;
+ function.setSigningParametersHandler(signingParametersHandler);
+ assert typeHeaderLookupStrategy != null;
+ function.setTypeHeaderLookupStrategy(typeHeaderLookupStrategy);
+ assert audienceLookupStrategy != null;
+ function.setAudienceLookupStrategy(audienceLookupStrategy);
+ function.initialize();
+ } catch (final ComponentInitializationException e) {
+ Assert.fail("Could not initialize the function", e);
+ }
+ }
+
+ @Test
+ public void testNullPrc() {
+ Assert.assertNull(function.apply(null, mockRefreshTokenClaimsSet()));
+ }
+
+ @Test
+ public void testNullInboundMessageContext() {
+ Assert.assertNull(function.apply(new ProfileRequestContext(), mockRefreshTokenClaimsSet()));
+ }
+
+ @Test
+ public void testNullClaimsSet() {
+ Assert.assertNull(function.apply(mockProfileRequestContext(), null));
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testSigningParametersHandlerException() throws MessageHandlerException {
+ Mockito.doThrow(MessageHandlerException.class).when(signingParametersHandler).invoke(Mockito.notNull());
+ Assert.assertNull(function.apply(mockProfileRequestContext(), mockRefreshTokenClaimsSet()));
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testSecurityParametersContextLookupStrategyReturningNull() throws MessageHandlerException {
+ Mockito.doNothing().when(signingParametersHandler).invoke(Mockito.any());
+ Mockito.when(securityParametersContextLookupStrategy.apply(Mockito.any())).thenReturn(null);
+ Assert.assertNull(function.apply(mockProfileRequestContext(), mockRefreshTokenClaimsSet()));
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testSecurityParametersContextLookupStrategyReturningNoParams() throws MessageHandlerException {
+ final SecurityParametersContext securityParametersContext = new SecurityParametersContext();
+ securityParametersContext.setSignatureSigningParameters(new SignatureSigningParameters());
+ Mockito.doNothing().when(signingParametersHandler).invoke(Mockito.any());
+ Mockito.when(securityParametersContextLookupStrategy.apply(Mockito.any()))
+ .thenReturn(new SecurityParametersContext());
+ Assert.assertNull(function.apply(mockProfileRequestContext(), mockRefreshTokenClaimsSet()));
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testNoAudience() throws MessageHandlerException {
+ final SecurityParametersContext securityParametersContext = new SecurityParametersContext();
+ securityParametersContext.setSignatureSigningParameters(new SignatureSigningParameters());
+ Mockito.doNothing().when(signingParametersHandler).invoke(Mockito.any());
+ Mockito.when(securityParametersContextLookupStrategy.apply(Mockito.any()))
+ .thenReturn(securityParametersContext);
+ Mockito.when(audienceLookupStrategy.apply(Mockito.any())).thenReturn(null);
+ Assert.assertNull(function.apply(mockProfileRequestContext(), mockRefreshTokenClaimsSet()));
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testSignerFailing() throws MessageHandlerException {
+ final SecurityParametersContext securityParametersContext = new SecurityParametersContext();
+ securityParametersContext.setSignatureSigningParameters(new SignatureSigningParameters());
+ Mockito.doNothing().when(signingParametersHandler).invoke(Mockito.any());
+ Mockito.when(securityParametersContextLookupStrategy.apply(Mockito.any()))
+ .thenReturn(securityParametersContext);
+ Mockito.when(audienceLookupStrategy.apply(Mockito.any())).thenReturn("mockAudience");
+ Assert.assertNull(function.apply(mockProfileRequestContext(), mockRefreshTokenClaimsSet()));
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testSuccess() throws MessageHandlerException, ParseException {
+ final SecurityParametersContext securityParametersContext = new SecurityParametersContext();
+ final SignatureSigningParameters signingParameters = new SignatureSigningParameters();
+ signingParameters.setSigningCredential(clientSecretCredential());
+ signingParameters.setSignatureAlgorithm("HS256");
+ securityParametersContext.setSignatureSigningParameters(signingParameters);
+ Mockito.doNothing().when(signingParametersHandler).invoke(Mockito.any());
+ Mockito.when(securityParametersContextLookupStrategy.apply(Mockito.any()))
+ .thenReturn(securityParametersContext);
+ Mockito.when(audienceLookupStrategy.apply(Mockito.any())).thenReturn("mockAudience");
+ final String result = function.apply(mockProfileRequestContext(), mockRefreshTokenClaimsSet());
+ final SignedJWT jwt = SignedJWT.parse(result);
+ Assert.assertNull(JWTSignatureValidationUtil.validateSignatureEx(List.of(clientSecretCredential()), jwt,
+ OidcEventIds.INVALID_GRANT));
+ }
+
+ protected ProfileRequestContext mockProfileRequestContext() {
+ final ProfileRequestContext profileRequestContext = new ProfileRequestContext();
+ profileRequestContext.setInboundMessageContext(new MessageContext());
+ return profileRequestContext;
+ }
+
+ @SuppressWarnings("null")
+ protected RefreshTokenClaimsSet mockRefreshTokenClaimsSet() {
+ return new RefreshTokenClaimsSet.Builder()
+ .setJWTID("mockId")
+ .setClientID(new ClientID("mockClientId"))
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(600))
+ .setIssuer("mockIssuer")
+ .setAuthenticationTime(Instant.now())
+ .setSubject("mockSubject")
+ .setScope(Scope.parse(""))
+ .build();
+ }
+
+ protected Credential clientSecretCredential() {
+ return new DefaultClientSecretCredential(clientSecret).toSigningCredential();
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index bdddb0ea..89255cec 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -85,6 +85,17 @@
</list>
</property>
</bean>
+ <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdNotMDDrivenRefreshTokenJwt">
+ <property name="profileConfigurations">
+ <list>
+ <ref bean="OIDC.SSO" />
+ <bean parent="OAUTH2.Token" p:refreshTokenType="JWT"/>
+ <ref bean="OIDC.UserInfo" />
+ <ref bean="OAUTH2.Introspection" />
+ <ref bean="OAUTH2.Revocation" />
+ </list>
+ </property>
+ </bean>
<bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdEncryptionEnforced">
<property name="profileConfigurations">
<list>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list