[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