[java-idp-oidc] branch dev/JOIDC-7 updated: Hack to seal some custom claims when JWTs are used.
Scott Cantor
cantor.2 at osu.edu
Tue Apr 26 20:28:01 UTC 2022
This is an automated email from the git hooks/post-receive script.
scantor pushed a commit to branch dev/JOIDC-7
in repository java-idp-oidc.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=daa1de305dd1ce81dd4a6572347f6b96d6fc7043
The following commit(s) were added to refs/heads/dev/JOIDC-7 by this push:
new daa1de30 Hack to seal some custom claims when JWTs are used.
daa1de30 is described below
commit daa1de305dd1ce81dd4a6572347f6b96d6fc7043
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Apr 26 16:27:59 2022 -0400
Hack to seal some custom claims when JWTs are used.
---
.../op/token/support/AccessTokenClaimsSet.java | 42 ++++++++++-
.../oidc/op/token/support/TokenClaimsSet.java | 3 +
.../op/oauth2/profile/impl/BuildAccessToken.java | 83 ++++++++++++++++++++--
.../op/userinfo/profile/impl/ParseAccessToken.java | 31 +++-----
.../META-INF/net.shibboleth.idp/postconfig.xml | 1 +
.../oauth2/profile/impl/BuildAccessTokenTest.java | 8 ++-
.../profile/impl/ParseAccessTokenTest.java | 7 +-
7 files changed, 145 insertions(+), 30 deletions(-)
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
index 81347dbe..bf34d641 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
@@ -20,6 +20,7 @@ package net.shibboleth.idp.plugin.oidc.op.token.support;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.ClientID;
@@ -34,6 +35,7 @@ import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrate
import java.net.URI;
import java.text.ParseException;
import java.time.Instant;
+import java.util.Map;
/** Class wrapping claims set for access token. */
public final class AccessTokenClaimsSet extends TokenClaimsSet {
@@ -69,7 +71,7 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
*
* @param accessTokenClaimsSet access token claims set
*/
- public AccessTokenClaimsSet(@Nonnull final JWTClaimsSet accessTokenClaimsSet) {
+ protected AccessTokenClaimsSet(@Nonnull final JWTClaimsSet accessTokenClaimsSet) {
super(accessTokenClaimsSet);
}
@@ -102,6 +104,44 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
return parse(dataSealer.unwrap(wrappedAccessToken));
}
+ /**
+ * Parses access token from sealed access token.
+ *
+ * @param jwtAccessToken wrapped access token
+ * @param dataSealer sealer to unwrap the access token
+ *
+ * @return access token claims set
+ *
+ * @throws ParseException is thrown if unwrapped access token is not understood
+ * @throws DataSealerException is thrown if unwrapping fails
+ *
+ * @since 3.2.0
+ */
+ @Nonnull public static AccessTokenClaimsSet parse(@Nonnull @NotEmpty final JWT jwtAccessToken,
+ @Nonnull final DataSealer dataSealer) throws ParseException, DataSealerException {
+
+ JWTClaimsSet claims = jwtAccessToken.getJWTClaimsSet();
+
+ // Check for embedded custom claim.
+ if (claims.getClaim(TokenClaimsSet.KEY_SEALED_FOR_OP) == null) {
+ // Throws exception if parsing result is not expected one.
+ verifyParsedClaims(VALUE_TYPE_AT, claims);
+ return new AccessTokenClaimsSet(claims);
+ }
+
+ final Map<String,Object> map = claims.toJSONObject();
+ final JWTClaimsSet unsealed = JWTClaimsSet.parse(
+ dataSealer.unwrap(claims.getStringClaim(TokenClaimsSet.KEY_SEALED_FOR_OP)));
+ map.remove(TokenClaimsSet.KEY_SEALED_FOR_OP);
+ for (Map.Entry<String,Object> claim : unsealed.getClaims().entrySet()) {
+ map.put(claim.getKey(), claim.getValue());
+ }
+
+ claims = JWTClaimsSet.parse(map);
+ verifyParsedClaims(VALUE_TYPE_AT, claims);
+ return new AccessTokenClaimsSet(claims);
+ }
+
/** Builder to create instance of AccessTokenClaimsSet. */
public static final class Builder extends TokenClaimsSet.Builder<AccessTokenClaimsSet> {
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
index 0c3b1790..ce81beb2 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
@@ -130,6 +130,9 @@ public class TokenClaimsSet {
/** Code Challenge. */
@Nonnull @NotEmpty public static final String KEY_CODE_CHALLENGE = "cc";
+ /** Custom claim name for sealed claims embedded inside JWT. */
+ @Nonnull @NotEmpty public static final String KEY_SEALED_FOR_OP = "for_op";
+
/** Claims set for the claim. */
@Nullable private JWTClaimsSet tokenClaimsSet;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
index 7460ed66..9017f8c7 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
@@ -17,8 +17,11 @@
package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
+import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -32,6 +35,7 @@ import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.PlainJWT;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.ClientID;
@@ -61,6 +65,8 @@ import net.shibboleth.oidc.profile.config.navigate.AccessTokenTypeLookupFunction
import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.action.ActionSupport;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
import net.shibboleth.utilities.java.support.logic.FunctionSupport;
@@ -96,7 +102,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
@Nonnull private Logger log = LoggerFactory.getLogger(BuildAccessToken.class);
/** Sealer to use for opaque tokens. */
- @Nullable private DataSealer dataSealer;
+ @NonnullAfterInit private DataSealer dataSealer;
/** Strategy used to obtain the response issuer value. */
@Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
@@ -291,6 +297,16 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
accessTokenContextCreationStrategy =
Constraint.isNotNull(strategy, "AccessTokenContext creation strategy cannot be null");
}
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (dataSealer == null) {
+ throw new ComponentInitializationException("DataSealer cannot be null");
+ }
+ }
/** {@inheritDoc} */
@Override
@@ -399,8 +415,6 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
final AccessTokenClaimsSet.Builder builder;
- // TODO: some of these custom claims need to be sealed if JWT format is used.
-
if (tokenClaimsSet != null) {
// We may not use original claims as input for scope / delivery claims as they may have been reduced.
builder = new AccessTokenClaimsSet.Builder(
@@ -453,16 +467,75 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
try {
if (jwtTokenType) {
- accessTokenCtx.setJWT(new PlainJWT(claimsSet.getClaimsSet()));
+ accessTokenCtx.setJWT(new PlainJWT(sealClaims(claimsSet.getClaimsSet())));
log.debug("{} Claims stored to JWT access token: {}", getLogPrefix(), claimsSet.serialize());
} else {
accessTokenCtx.setOpaque(claimsSet.serialize(dataSealer));
log.debug("{} Claims converted to opaque access token: {}", getLogPrefix(), claimsSet.serialize());
}
- } catch (final DataSealerException e) {
+ } catch (final DataSealerException | ParseException e) {
log.error("{} Access Token wrapping failed: {}", getLogPrefix(), e);
ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
}
}
+ /**
+ * Rewrites a plaintext claimsset to hide custom claims used solely by the OP.
+ *
+ * @param claims the input claims
+ *
+ * @return a rewritten claims set to use for the access token
+ *
+ * @throws ParseException if unable to parse a claims set
+ * @throws DataSealerException if unable to seal the custom claims
+ */
+ @Nonnull private JWTClaimsSet sealClaims(@Nonnull final JWTClaimsSet claims) throws DataSealerException, ParseException {
+
+ // Rewrite as a mutable map.
+ final Map<String,Object> map = claims.toJSONObject();
+
+ // Put the claims to hide here.
+ final Map<String,Object> toSeal = new HashMap<>();
+
+ if (map.containsKey(TokenClaimsSet.KEY_USER_PRINCIPAL)) {
+ toSeal.put(TokenClaimsSet.KEY_USER_PRINCIPAL, map.remove(TokenClaimsSet.KEY_USER_PRINCIPAL));
+ }
+
+ if (map.containsKey(TokenClaimsSet.KEY_DELIVERY_CLAIMS)) {
+ toSeal.put(TokenClaimsSet.KEY_DELIVERY_CLAIMS, map.remove(TokenClaimsSet.KEY_DELIVERY_CLAIMS));
+ }
+
+ if (map.containsKey(TokenClaimsSet.KEY_DELIVERY_CLAIMS_USERINFO)) {
+ toSeal.put(TokenClaimsSet.KEY_DELIVERY_CLAIMS_USERINFO, map.remove(TokenClaimsSet.KEY_DELIVERY_CLAIMS_USERINFO));
+ }
+
+ if (map.containsKey(TokenClaimsSet.KEY_CONSENTED_CLAIMS)) {
+ toSeal.put(TokenClaimsSet.KEY_CONSENTED_CLAIMS, map.remove(TokenClaimsSet.KEY_CONSENTED_CLAIMS));
+ }
+
+ if (map.containsKey(TokenClaimsSet.KEY_CONSENT_ENABLED)) {
+ toSeal.put(TokenClaimsSet.KEY_CONSENT_ENABLED, map.remove(TokenClaimsSet.KEY_CONSENT_ENABLED));
+ }
+
+ if (map.containsKey(TokenClaimsSet.KEY_CODE_CHALLENGE)) {
+ toSeal.put(TokenClaimsSet.KEY_CODE_CHALLENGE, map.remove(TokenClaimsSet.KEY_CODE_CHALLENGE));
+ }
+
+ if (map.containsKey(TokenClaimsSet.KEY_NONCE)) {
+ toSeal.put(TokenClaimsSet.KEY_NONCE, map.remove(TokenClaimsSet.KEY_NONCE));
+ }
+
+ if (toSeal.isEmpty()) {
+ // Nothing to do.
+ return claims;
+ }
+
+ // Wrap the sealed claims and re-embed back in original claims set.
+ final String sealed = dataSealer.wrap(JWTClaimsSet.parse(toSeal).toString());
+ map.put(TokenClaimsSet.KEY_SEALED_FOR_OP, sealed);
+
+ // Re-parse the claims.
+ return JWTClaimsSet.parse(map);
+ }
+
}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessToken.java
index d2d5bbf7..54989a7f 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessToken.java
@@ -35,7 +35,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.nimbusds.jose.JOSEObjectType;
-import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.token.AccessToken;
@@ -82,9 +81,6 @@ public class ParseAccessToken extends AbstractOIDCUserInfoValidationResponseActi
/** Copy of signed JWT for non-opaque access tokens. */
@Nullable private SignedJWT signedJWT;
-
- /** Our local type used for opaque tokens. */
- @Nullable private AccessTokenClaimsSet opaqueClaimsSet;
/**
* Set the data sealer instance to use.
@@ -112,7 +108,7 @@ public class ParseAccessToken extends AbstractOIDCUserInfoValidationResponseActi
super.doInitialize();
if (dataSealer == null) {
- throw new ComponentInitializationException("RevocationCache and DataSealer cannot be null");
+ throw new ComponentInitializationException("DataSealer cannot be null");
}
}
@@ -127,14 +123,14 @@ public class ParseAccessToken extends AbstractOIDCUserInfoValidationResponseActi
return;
}
- final JWTClaimsSet tokenClaimsSet = parseAccessToken(token);
- if (tokenClaimsSet == null) {
+ final AccessTokenClaimsSet accessTokenClaimsSet = parseAccessToken(token);
+ if (accessTokenClaimsSet == null) {
log.warn("{} Unable to parse/decode token for validation", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
return;
}
- log.debug("{} Access token unwrapped: {}", getLogPrefix(), tokenClaimsSet.toString());
+ log.debug("{} Access token unwrapped: {}", getLogPrefix(), accessTokenClaimsSet.serialize());
if (signedJWT != null) {
// Check typ header.
@@ -167,18 +163,14 @@ public class ParseAccessToken extends AbstractOIDCUserInfoValidationResponseActi
final String errorEventId = JWTSignatureValidationUtil.validateSignatureEx(credList, signedJWT,
OidcEventIds.INVALID_GRANT);
if (errorEventId != null) {
- log.warn("{} Signature on token ID '{}' invalid", getLogPrefix(), tokenClaimsSet.getJWTID());
+ log.warn("{} Signature on token ID '{}' invalid", getLogPrefix(), accessTokenClaimsSet.getID());
ActionSupport.buildEvent(profileRequestContext, errorEventId);
return;
}
}
- log.debug("{} Access token {} parsed", getLogPrefix(), tokenClaimsSet.getJWTID());
- if (opaqueClaimsSet == null) {
- // Wraps the JWT claims in a custom object.
- opaqueClaimsSet = new AccessTokenClaimsSet(tokenClaimsSet);
- }
- getOidcResponseContext().setAuthorizationGrantClaimsSet(opaqueClaimsSet);
+ log.debug("{} Access token {} parsed", getLogPrefix(), accessTokenClaimsSet.getID());
+ getOidcResponseContext().setAuthorizationGrantClaimsSet(accessTokenClaimsSet);
}
/**
@@ -188,20 +180,19 @@ public class ParseAccessToken extends AbstractOIDCUserInfoValidationResponseActi
*
* @return parsed claim set or null
*/
- @Nullable protected JWTClaimsSet parseAccessToken(@Nonnull @NotEmpty final AccessToken token) {
+ @Nullable protected AccessTokenClaimsSet parseAccessToken(@Nonnull @NotEmpty final AccessToken token) {
// Try parsing as a JWT.
try {
signedJWT = SignedJWT.parse(token.getValue());
- return signedJWT.getJWTClaimsSet();
- } catch (final ParseException e1) {
+ return AccessTokenClaimsSet.parse(signedJWT, dataSealer);
+ } catch (final DataSealerException | ParseException e) {
}
// Fall back to opaque.
try {
- opaqueClaimsSet = AccessTokenClaimsSet.parse(token.getValue(), dataSealer);
- return opaqueClaimsSet.getClaimsSet();
+ return AccessTokenClaimsSet.parse(token.getValue(), dataSealer);
} catch (final DataSealerException | ParseException e) {
}
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 394f3440..ce0505dc 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -157,6 +157,7 @@
<value>acr</value>
<value>amr</value>
<value>auth_time</value>
+ <value>for_op</value>
</list>
</property>
</bean>
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
index f94d9f1b..c4acd64c 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
@@ -145,9 +145,8 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
throws ComponentInitializationException, NoSuchAlgorithmException {
if ("JWT".equals(type)) {
action.setAccessTokenTypeLookupStrategy(FunctionSupport.constant("JWT"));
- } else if (type == null) {
- action.setDataSealer(getDataSealer());
}
+ action.setDataSealer(getDataSealer());
action.setClientIDLookupStrategy(FunctionSupport.constant(new ClientID(clientId)));
action.initialize();
}
@@ -195,7 +194,10 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
assertTrue(claims.getIssueTime().toInstant().isBefore(Instant.now()));
assertEquals(claims.getStringClaim(TokenClaimsSet.KEY_SCOPE), scope.toString());
assertEquals(claims.getSubject(), clientId);
- assertEquals(claims.getStringClaim(TokenClaimsSet.KEY_USER_PRINCIPAL), "jdoe");
+
+ final JWTClaimsSet unsealedClaims =
+ JWTClaimsSet.parse(getDataSealer().unwrap(claims.getStringClaim(TokenClaimsSet.KEY_SEALED_FOR_OP)));
+ assertEquals(unsealedClaims.getStringClaim(TokenClaimsSet.KEY_USER_PRINCIPAL), "jdoe");
} else {
throw new RuntimeException("No token found");
}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessTokenTest.java
index 65e99628..0f6c64bf 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessTokenTest.java
@@ -51,6 +51,7 @@ import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.ClientID;
@@ -149,17 +150,21 @@ public class ParseAccessTokenTest extends BaseOIDCResponseActionTest {
@Test
public void testJWT()
throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException, JOSEException {
+
+ final String sealedClaims = getDataSealer().wrap(
+ new JWTClaimsSet.Builder().claim(TokenClaimsSet.KEY_USER_PRINCIPAL, "userPrin").build().toString());
+
final TokenClaimsSet claims = new AccessTokenClaimsSet.Builder()
.setJWTID(idGenerator)
.setClientID(new ClientID())
.setIssuer("issuer")
- .setPrincipal("userPrin")
.setSubject("subject")
.setIssuedAt(Instant.now())
.setExpiresAt(Instant.now().plusSeconds(1))
.setAuthenticationTime(Instant.now())
.setRedirectURI(new URI("http://example.com"))
.setScope(new Scope())
+ .addCustomClaim(TokenClaimsSet.KEY_SEALED_FOR_OP, sealedClaims)
.build();
JWSSigner signer = new RSASSASigner(rsaPrivateKey);
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list