[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