[java-idp-oidc] branch main updated: Add token_type hints and audience to introspection.

Scott Cantor cantor.2 at osu.edu
Wed Jan 5 19:37:42 UTC 2022


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch main
in repository java-idp-oidc.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=fd4f3e320b24ee6d4bb2249f4d6c1ff272f45276

The following commit(s) were added to refs/heads/main by this push:
     new fd4f3e32 Add token_type hints and audience to introspection.
fd4f3e32 is described below

commit fd4f3e320b24ee6d4bb2249f4d6c1ff272f45276
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Jan 5 14:37:39 2022 -0500

    Add token_type hints and audience to introspection.
---
 .../FormOutboundIntrospectionResponseMessage.java  | 121 +++++++++++++--------
 .../op/profile/flow/IntrospectionFlowTest.java     |  35 +++---
 2 files changed, 96 insertions(+), 60 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutboundIntrospectionResponseMessage.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutboundIntrospectionResponseMessage.java
index 490f6823..6a9cd2af 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutboundIntrospectionResponseMessage.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutboundIntrospectionResponseMessage.java
@@ -18,17 +18,23 @@
 package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
 
 import java.text.ParseException;
+import java.util.Collections;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import com.nimbusds.oauth2.sdk.TokenIntrospectionRequest;
 import com.nimbusds.oauth2.sdk.TokenIntrospectionSuccessResponse;
+import com.nimbusds.oauth2.sdk.id.Audience;
 import com.nimbusds.oauth2.sdk.id.Issuer;
 import com.nimbusds.oauth2.sdk.id.Subject;
+import com.nimbusds.oauth2.sdk.token.AccessToken;
 import com.nimbusds.oauth2.sdk.token.AccessTokenType;
+import com.nimbusds.oauth2.sdk.token.RefreshToken;
+import com.nimbusds.oauth2.sdk.token.Token;
 
 import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCRequestAction;
 import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCache;
@@ -38,6 +44,7 @@ import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
 import net.shibboleth.utilities.java.support.annotation.ParameterName;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -51,16 +58,13 @@ import net.shibboleth.utilities.java.support.security.DataSealerException;
 public class FormOutboundIntrospectionResponseMessage extends AbstractOIDCRequestAction<TokenIntrospectionRequest> {
 
     /** Class logger. */
-    @Nonnull
-    private Logger log = LoggerFactory.getLogger(FormOutboundIntrospectionResponseMessage.class);
+    @Nonnull private Logger log = LoggerFactory.getLogger(FormOutboundIntrospectionResponseMessage.class);
 
     /** Data sealer for unwrapping token. */
-    @Nonnull
-    private final DataSealer dataSealer;
+    @Nonnull private final DataSealer dataSealer;
 
     /** Message revocation cache instance to use. */
-    @NonnullAfterInit
-    private RevocationCache revocationCache;
+    @NonnullAfterInit private RevocationCache revocationCache;
 
     /**
      * Constructor.
@@ -91,53 +95,82 @@ public class FormOutboundIntrospectionResponseMessage extends AbstractOIDCReques
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        TokenClaimsSet tokenClaimsSet = null;
-        AccessTokenType tokenType = null;
-        log.debug("{} token to introspect {}", getLogPrefix(), getRequest().getToken().getValue());
-        try {
-            tokenClaimsSet = AccessTokenClaimsSet.parse(getRequest().getToken().getValue(), dataSealer);
-            tokenType = AccessTokenType.BEARER;
-            log.debug("{} access token unwrapped {}", getLogPrefix(), tokenClaimsSet.serialize());
-        } catch (final DataSealerException | ParseException e) {
-            log.debug("{} token to introspect is not valid access token", getLogPrefix());
-        }
-        if (tokenClaimsSet == null) {
-            try {
-                tokenClaimsSet = RefreshTokenClaimsSet.parse(getRequest().getToken().getValue(), dataSealer);
-                log.debug("{} refresh token unwrapped {}", getLogPrefix(), tokenClaimsSet.serialize());
-            } catch (final DataSealerException | ParseException e) {
-                log.debug("{} token to introspect is not valid refresh token", getLogPrefix());
-            }
-        }
+        
+        log.debug("{} Token to introspect: {}", getLogPrefix(), getRequest().getToken().getValue());
+        
+        final TokenClaimsSet tokenClaimsSet = parseToken(getRequest().getToken());
         if (tokenClaimsSet == null) {
-            log.debug("{} unable to decode token", getLogPrefix());
-            profileRequestContext.getOutboundMessageContext()
-                    .setMessage(new TokenIntrospectionSuccessResponse.Builder(false).build());
-            return;
-        }
-        if (revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, tokenClaimsSet.getID())) {
-            log.debug("{} tokens derived from authorization code {} are all revoked", getLogPrefix(),
-                    tokenClaimsSet.getID());
+            log.debug("{} Unable to decode token", getLogPrefix());
             profileRequestContext.getOutboundMessageContext()
                     .setMessage(new TokenIntrospectionSuccessResponse.Builder(false).build());
             return;
         }
+
+        log.debug("{} {} token unsealed: {}", getLogPrefix(),
+                tokenClaimsSet instanceof AccessTokenClaimsSet ? "Access" : "Refresh", tokenClaimsSet.serialize());
+        
         if (tokenClaimsSet.isExpired()) {
-            log.debug("{} tokens is expired", getLogPrefix(), tokenClaimsSet.getID());
-            profileRequestContext.getOutboundMessageContext()
-                    .setMessage(new TokenIntrospectionSuccessResponse.Builder(false).build());
+            log.debug("{} Token ID {} is expired", getLogPrefix(), tokenClaimsSet.getID());
+            profileRequestContext.getOutboundMessageContext().setMessage(
+                    new TokenIntrospectionSuccessResponse.Builder(false).build());
+            return;
+        } else if (revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, tokenClaimsSet.getID())) {
+            log.debug("{} Token ID {} is revoked", getLogPrefix(), tokenClaimsSet.getID());
+            profileRequestContext.getOutboundMessageContext().setMessage(
+                    new TokenIntrospectionSuccessResponse.Builder(false).build());
             return;
         }
-        // TODO: We are not returning audience field at all. Audience would be useful for example in cases where
-        // Resource Server
-        // uses introspection to verify it is on the audience list before accepting the request.
-        // Audience information is not currently carried in tokens.
-        profileRequestContext.getOutboundMessageContext()
-                .setMessage(new TokenIntrospectionSuccessResponse.Builder(true).scope(tokenClaimsSet.getScope())
-                        .clientID(tokenClaimsSet.getClientID()).username(tokenClaimsSet.getPrincipal())
-                        .tokenType(tokenType).expirationTime(tokenClaimsSet.getClaimsSet().getExpirationTime())
+        
+        // Audience information is not currently carried in tokens but can be inferred as self-targeted for now.
+        
+        profileRequestContext.getOutboundMessageContext().setMessage(
+                new TokenIntrospectionSuccessResponse.Builder(true)
+                        .scope(tokenClaimsSet.getScope())
+                        .clientID(tokenClaimsSet.getClientID())
+                        .username(tokenClaimsSet.getPrincipal())
+                        .tokenType(AccessTokenType.BEARER)
+                        .expirationTime(tokenClaimsSet.getClaimsSet().getExpirationTime())
                         .issueTime(tokenClaimsSet.getClaimsSet().getIssueTime())
                         .subject(new Subject(tokenClaimsSet.getClaimsSet().getSubject()))
-                        .issuer(new Issuer(tokenClaimsSet.getClaimsSet().getIssuer())).build());
+                        .issuer(new Issuer(tokenClaimsSet.getClaimsSet().getIssuer()))
+                        .audience(Collections.singletonList(new Audience(tokenClaimsSet.getClaimsSet().getIssuer())))
+                        .build());
     }
+
+    /**
+     * Attempt to parse token.
+     * 
+     * @param token the token
+     * 
+     * @return parsed claim set or null
+     */
+    @Nullable protected TokenClaimsSet parseToken(@Nonnull @NotEmpty final Token token) {
+        try {
+            if (token instanceof AccessToken) {
+                return AccessTokenClaimsSet.parse(token.getValue(), dataSealer);
+            } else if (token instanceof RefreshToken) {
+                return RefreshTokenClaimsSet.parse(token.getValue(), dataSealer);
+            }
+        } catch (final DataSealerException | ParseException e) {
+            
+        }
+        
+        // Token type hint missing, have to try both.
+        try {
+            return AccessTokenClaimsSet.parse(token.getValue(), dataSealer);
+        } catch (final DataSealerException | ParseException e) {
+            
+        }
+        
+        try {
+            return RefreshTokenClaimsSet.parse(token.getValue(), dataSealer);
+        } catch (final DataSealerException | ParseException e) {
+            
+        }
+        
+        log.debug("{} Token to introspect is invalid or unknown", getLogPrefix());
+        
+        return null;
+    }
+
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
index 7156f0ad..fe06694f 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
@@ -20,9 +20,7 @@ package net.shibboleth.idp.plugin.oidc.op.profile.flow;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
-import java.time.Instant;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -41,6 +39,7 @@ import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.TokenIntrospectionErrorResponse;
 import com.nimbusds.oauth2.sdk.TokenIntrospectionSuccessResponse;
 import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.id.Audience;
 
 import net.shibboleth.utilities.java.support.collection.Pair;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
@@ -53,18 +52,18 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
 
     public static final String FLOW_ID = "oauth2/introspection";
 
-    String clientId = "mockClientId";
+    private String clientId = "mockClientId";
 
-    String clientSecret = "mockClientSecret";
+    private String clientSecret = "mockClientSecret";
 
-    String clientIdSaml = "mockSamlClientId";
-    String clientSecretSaml = "mockClientSecretmockClientSecretmockClientSecret";
+    private String clientIdSaml = "mockSamlClientId";
+    private String clientSecretSaml = "mockClientSecretmockClientSecretmockClientSecret";
 
-    Scope scope = Scope.parse("openid profile email");
+    private Scope scope = Scope.parse("openid profile email");
     
     @Autowired
     @Qualifier("shibboleth.StorageService")
-    StorageService storageService;
+    private StorageService storageService;
     
     public IntrospectionFlowTest() {
         super(FLOW_ID);
@@ -101,23 +100,27 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
             ComponentInitializationException {
         storeMetadata(storageService, clientId, clientSecret, scope);
         setBasicAuth(clientId, clientSecret);
-        setHttpFormRequest("POST", Collections.singletonMap("token",
-                super.buildToken(clientId, "sub", Scope.parse("openid")).toJSONObject().getAsString("access_token")));
+        setHttpFormRequest("POST", Map.of(
+                "token",
+                super.buildToken(clientId, "sub", Scope.parse("openid")).toJSONObject().getAsString("access_token"),
+                "token_type",
+                "access_token"));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         final TokenIntrospectionSuccessResponse resp =
                 parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
-        Assert.assertEquals(resp.getClientID().getValue(), clientId);
         Assert.assertTrue(resp.isActive());
+        Assert.assertEquals(resp.getClientID().getValue(), clientId);
+        Assert.assertEquals(resp.getAudience(), Collections.singletonList(new Audience("https://op.example.org")));
     }
 
     @Test
-    public void testSuccessWithSamlMetadata() throws IOException, NoSuchAlgorithmException, URISyntaxException, DataSealerException,
+    public void testSuccessWithSamlMetadata() throws NoSuchAlgorithmException, URISyntaxException, DataSealerException,
             ComponentInitializationException {
         setBasicAuth(clientIdSaml, clientSecretSaml);
         setHttpFormRequest("POST", Collections.singletonMap("token",
                 super.buildToken(clientIdSaml, "sub", Scope.parse("openid")).toJSONObject().getAsString("access_token")));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        TokenIntrospectionSuccessResponse resp =
+        final TokenIntrospectionSuccessResponse resp =
                 parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
         Assert.assertEquals(resp.getClientID().getValue(), clientIdSaml);
         Assert.assertTrue(resp.isActive());
@@ -193,10 +196,10 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
         return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
     }
     
-    protected Map<String,String> createRequestParameters(final String token, final String clientId) {
+    protected Map<String,String> createRequestParameters(final String token, final String id) {
         final Map<String,String> result = new HashMap<>();
         result.put("token", token);
-        result.put("client_id", clientId);
+        result.put("client_id", id);
         return result;
     }
 
@@ -204,4 +207,4 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
         return new Pair<>("invalid_client", "Client authentication failed");
     }
 
-}
+}
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list