[java-idp-plugin-oidc-rp] branch main updated: Change Token and UserInfo response types
Phil Smart
philip.smart at jisc.ac.uk
Mon Mar 13 16:29:25 UTC 2023
This is an automated email from the git hooks/post-receive script.
philsmart pushed a commit to branch main
in repository java-idp-plugin-oidc-rp.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-oidc-rp.git;a=commit;h=8195d7df6ea5010977287c465f25d0a4333b45c8
The following commit(s) were added to refs/heads/main by this push:
new 8195d7d Change Token and UserInfo response types
8195d7d is described below
commit 8195d7df6ea5010977287c465f25d0a4333b45c8
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Mon Mar 13 16:29:22 2023 +0000
Change Token and UserInfo response types
- Change to the Nimbus version of these response types
- Internal changes only. No change to RP behaviour
- The change propagated to a large number of classes in the project
- Remove old response types
- Update tests
---
idp-oidc-rp-api/pom.xml | 7 +-
.../idp/plugin/authn/oidc/rp/OIDCRPException.java | 3 -
.../rp/context/AccessTokenResponseContext.java | 47 +++--
.../oidc/rp/context/UserInfoResponseContext.java | 9 +-
...trategy.java => AccessTokenLookupStrategy.java} | 24 ++-
.../navigate/DefaultIDTokenLookupStrategy.java | 4 +-
....java => DefaultUserInfoJWTLookupStrategy.java} | 20 +-
.../IDTokenInAccessTokenUpdateStrategy.java | 13 +-
.../navigate/IDTokenJOSEHeaderLookupStrategy.java | 5 +-
.../navigate/RequestObjectTokenUpdateStrategy.java | 3 +-
.../navigate/SubFromIDTokenLookupFunction.java | 4 +-
.../TokenResponseIDTokenLookupStrategy.java | 63 -------
...nfoInUserInfoResponseContextUpdateStrategy.java | 7 +-
.../oidc/rp/messaging/JWTUserInfoResponse.java | 112 ------------
.../oidc/rp/messaging/PlainUserInfoResponse.java | 81 ---------
.../ResponseParsingException.java} | 44 ++---
.../authn/oidc/rp/messaging/UserInfoResponse.java | 87 ---------
.../UserInfoEncryptedJWTResponseTypeCondition.java | 10 +-
.../logic/UserInfoPlainResponseTypeCondition.java | 6 +-
.../UserInfoSignedJWTResponseTypeCondition.java | 9 +-
.../IDTokenInAccessTokenUpdateStrategyTest.java | 92 ++++++++++
.../oidc/rp/messaging/JWTUserInfoResponseTest.java | 119 ------------
.../authn/oidc/rp/test}/TestTokenHelper.java | 2 +-
idp-oidc-rp-impl/pom.xml | 6 +
.../impl/DefaultAccessTokenResponseDecoder.java | 91 ++++++++++
.../decoding/impl/DefaultMapResponseDecoder.java | 68 -------
.../impl/DefaultUserInfoResponseDecoder.java | 121 ++++++-------
.../impl/DefaultUserInfoRequestEncoder.java | 15 +-
.../impl/AbstractHttpOIDCAuthenticationAction.java | 37 +++-
.../oidc/rp/impl/ExchangeCodeForAccessToken.java | 22 ++-
.../oidc/rp/impl/ExtractIDTokenFromResponse.java | 156 ----------------
.../authn/oidc/rp/impl/ProcessEndUserClaims.java | 26 ++-
.../authn/oidc/rp/impl/UserInfoEndpointLookup.java | 13 +-
.../rp/impl/ValidateOAuthAccessTokenResponse.java | 52 +++---
.../rp/impl/ValidateUserInfoJSONObjectClaims.java | 61 +++++--
.../oidc-relying-party-authn-beans.xml | 28 +--
.../oidc-relying-party-authn-flow.xml | 1 -
... => DefaultAccessTokenResponseDecoderTest.java} | 53 ++++--
.../impl/DefaultUserInfoResponseDecoderTest.java | 94 +++++++---
.../authn/oidc/rp/impl/AbstractOIDCTest.java | 48 +++++
...st.java => ExchangeCodeForAccessTokenTest.java} | 123 ++++++++++++-
.../rp/impl/ExtractIDTokenFromResponseTest.java | 103 -----------
.../rp/impl/MergeUserInfoAndIDTokenClaimsTest.java | 126 -------------
.../OIDCRPFlowFromAuthenticationResponseTest.java | 36 ++++
.../plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java | 19 +-
.../oidc/rp/impl/ProcessEndUserClaimsTest.java | 84 +++++++--
.../TokenResponseIDTokenLookupStrategyTest.java | 75 --------
.../oidc/rp/impl/UserInfoEndpointLookupTest.java | 90 ++++++---
.../impl/ValidateOAuthAccessTokenResponseTest.java | 201 ++++++++++-----------
pom.xml | 9 +-
50 files changed, 1085 insertions(+), 1444 deletions(-)
diff --git a/idp-oidc-rp-api/pom.xml b/idp-oidc-rp-api/pom.xml
index 0535464..7b7d645 100644
--- a/idp-oidc-rp-api/pom.xml
+++ b/idp-oidc-rp-api/pom.xml
@@ -66,7 +66,12 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<scope>provided</scope>
- </dependency>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <scope>provided</scope>
+ </dependency>
</dependencies>
</project>
\ No newline at end of file
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/OIDCRPException.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/OIDCRPException.java
index 32a33e5..85fde94 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/OIDCRPException.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/OIDCRPException.java
@@ -65,7 +65,4 @@ public class OIDCRPException extends Exception{
}
-
-
-
}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/AccessTokenResponseContext.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/AccessTokenResponseContext.java
index 3bc8f0f..15586f1 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/AccessTokenResponseContext.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/AccessTokenResponseContext.java
@@ -17,11 +17,13 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.context;
-import java.util.Map;
+import java.time.Instant;
import javax.annotation.Nullable;
-import com.nimbusds.jwt.JWT;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+
+
/**
* A context to hold an OIDC token request response. If authenticated, the Token was received over a TLS protected
@@ -29,46 +31,39 @@ import com.nimbusds.jwt.JWT;
*/
public class AccessTokenResponseContext extends AbstractAuthenticatableOIDCContext {
- /** The raw token response as a map.*/
- @Nullable private Map<String, Object> rawTokenResponse;
+ /** The OAuth 2.0 and OIDC token response.*/
+ @Nullable private OIDCTokenResponse tokenResponse;
- /** The id_token, should match that in the rawTokenResponse if one exists.*/
- @Nullable private JWT idToken;
+ /** The time the token response was set onto this context.*/
+ @Nullable private Instant tokenResponseCreatedAt;
- /**
- * Set the raw token response as a map.
- *
- * @param rawResponse the raw response.
- */
- public void setRawTokenResponse(@Nullable final Map<String, Object> rawResponse) {
- rawTokenResponse = rawResponse;
- }
/**
- * Get the raw token response map.
+ * Set the OAuth 2.0 and OIDC Token Response.
*
- * @return the raw response map.
+ * @param token the token response
*/
- @Nullable public Map<String, Object> getRawTokenResponse() {
- return rawTokenResponse;
+ public void setTokenResponse(@Nullable final OIDCTokenResponse token) {
+ tokenResponse = token;
+ tokenResponseCreatedAt = Instant.now();
}
/**
- * Set the id_token.
+ * Get the OAuth 2.0 and OIDC Token Response.
*
- * @param token the id_token.
+ * @return the token response
*/
- public void setIdToken(@Nullable final JWT token) {
- idToken = token;
+ @Nullable public OIDCTokenResponse getTokenResponse() {
+ return tokenResponse;
}
/**
- * Get the id_token.
+ * Get the time the token response was set onto this context.
*
- * @return the id_token.
+ * @return the time the token response was set onto this context
*/
- @Nullable public JWT getIdToken() {
- return idToken;
+ @Nullable public Instant getTokenResponseCreatedAt() {
+ return tokenResponseCreatedAt;
}
}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/UserInfoResponseContext.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/UserInfoResponseContext.java
index e218580..8b3ef21 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/UserInfoResponseContext.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/UserInfoResponseContext.java
@@ -19,21 +19,20 @@ package net.shibboleth.idp.plugin.authn.oidc.rp.context;
import javax.annotation.Nullable;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse;
-
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
/** A context to hold the response from the UserInfo endpoint.*/
public class UserInfoResponseContext extends AbstractAuthenticatableOIDCContext {
/** The UserInfo response.*/
- @Nullable private UserInfoResponse userInfo;
+ @Nullable private UserInfoSuccessResponse userInfo;
/**
* Get the user info response.
*
* @return the user info.
*/
- @Nullable public UserInfoResponse getUserInfo() {
+ @Nullable public UserInfoSuccessResponse getUserInfo() {
return userInfo;
}
@@ -42,7 +41,7 @@ public class UserInfoResponseContext extends AbstractAuthenticatableOIDCContext
*
* @param info the user info response.
*/
- public void setUserInfo(@Nullable final UserInfoResponse info) {
+ public void setUserInfo(@Nullable final UserInfoSuccessResponse info) {
userInfo = info;
}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/RawTokenResponseLookupStrategy.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/AccessTokenLookupStrategy.java
similarity index 74%
rename from idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/RawTokenResponseLookupStrategy.java
rename to idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/AccessTokenLookupStrategy.java
index 100318d..4661edf 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/RawTokenResponseLookupStrategy.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/AccessTokenLookupStrategy.java
@@ -17,7 +17,6 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.context.navigate;
-import java.util.Map;
import java.util.function.Function;
import javax.annotation.Nonnull;
@@ -27,21 +26,29 @@ import javax.annotation.concurrent.ThreadSafe;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.oauth2.sdk.token.AccessToken;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
import net.shibboleth.utilities.java.support.annotation.ParameterName;
import net.shibboleth.utilities.java.support.logic.Constraint;
-/** Function that extracts the raw token response from the {@link AccessTokenResponseContext}.*/
+/** Function that extracts the access_token from the {@link AccessTokenResponseContext}.*/
@ThreadSafe
-public class RawTokenResponseLookupStrategy implements Function<ProfileRequestContext, Map<String, Object>> {
+public class AccessTokenLookupStrategy implements Function<ProfileRequestContext, AccessToken> {
+
+ /** Logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(AccessTokenLookupStrategy.class);
/** Strategy used to locate the {@link AccessTokenResponseContext} to extract the id_token from.*/
@Nonnull
private final Function<ProfileRequestContext, AccessTokenResponseContext> tokenResponseContextLookupStrategy;
/** Constructor.*/
- public RawTokenResponseLookupStrategy() {
+ public AccessTokenLookupStrategy() {
tokenResponseContextLookupStrategy =
new ChildContextLookup<>(AccessTokenResponseContext.class, true).compose(
new InboundMessageContextLookup());
@@ -53,19 +60,20 @@ public class RawTokenResponseLookupStrategy implements Function<ProfileRequestCo
*
* @param strategy the AccessTokenResponseContext lookup strategy to use.
*/
- public RawTokenResponseLookupStrategy(@ParameterName(name = "strategy")
+ public AccessTokenLookupStrategy(@ParameterName(name = "strategy")
@Nonnull final Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
tokenResponseContextLookupStrategy =
Constraint.isNotNull(strategy, "AccessTokenResponseContext lookup strategy can not be null");
}
@Override
- @Nullable public Map<String, Object> apply(@Nonnull final ProfileRequestContext prc) {
+ @Nullable public AccessToken apply(@Nonnull final ProfileRequestContext prc) {
final AccessTokenResponseContext tokenContext = tokenResponseContextLookupStrategy.apply(prc);
- if (tokenContext == null) {
+ if (tokenContext == null || tokenContext.getTokenResponse() == null ||
+ tokenContext.getTokenResponse().getTokens() == null) {
return null;
}
- return tokenContext.getRawTokenResponse();
+ return tokenContext.getTokenResponse().getTokens().getAccessToken();
}
}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/DefaultIDTokenLookupStrategy.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/DefaultIDTokenLookupStrategy.java
index ccf56ed..07ed3ce 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/DefaultIDTokenLookupStrategy.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/DefaultIDTokenLookupStrategy.java
@@ -62,10 +62,10 @@ public class DefaultIDTokenLookupStrategy implements Function<ProfileRequestCont
@Override
@Nullable public JWT apply(@Nonnull final ProfileRequestContext prc) {
final AccessTokenResponseContext tokenContext = tokenResponseContextLookupStrategy.apply(prc);
- if (tokenContext == null) {
+ if (tokenContext == null || tokenContext.getTokenResponse() == null) {
return null;
}
- return tokenContext.getIdToken();
+ return tokenContext.getTokenResponse().getOIDCTokens().getIDToken();
}
}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/DefaultUserInfoTokenLookupStrategy.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/DefaultUserInfoJWTLookupStrategy.java
similarity index 77%
rename from idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/DefaultUserInfoTokenLookupStrategy.java
rename to idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/DefaultUserInfoJWTLookupStrategy.java
index e19587c..e5e0919 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/DefaultUserInfoTokenLookupStrategy.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/DefaultUserInfoJWTLookupStrategy.java
@@ -27,25 +27,25 @@ import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import com.nimbusds.common.contenttype.ContentType;
import com.nimbusds.jwt.JWT;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.JWTUserInfoResponse;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse.UserInfoResponseType;
import net.shibboleth.utilities.java.support.logic.Constraint;
-/** Function that extracts the id_token from the {@link AccessTokenResponseContext}.*/
-//TODO could this be replaced by a ContextDataLookupFunction?
+/**
+ * Function that extracts the UserInfo claims as a JWT from the {@link UserInfoResponseContext}. If not found, returns
+ * {@code null}.
+ */
@ThreadSafe
-public class DefaultUserInfoTokenLookupStrategy implements Function<ProfileRequestContext, JWT> {
+public class DefaultUserInfoJWTLookupStrategy implements Function<ProfileRequestContext, JWT> {
/** Strategy used to look up the {@link UserInfoResponseContext}. */
@Nonnull private final Function<ProfileRequestContext, UserInfoResponseContext>
userInfoResponseContextLookupStrategy;
/** Constructor.*/
- public DefaultUserInfoTokenLookupStrategy() {
+ public DefaultUserInfoJWTLookupStrategy() {
userInfoResponseContextLookupStrategy =
new ChildContextLookup<>(UserInfoResponseContext.class).compose(
new InboundMessageContextLookup());
@@ -57,7 +57,7 @@ public class DefaultUserInfoTokenLookupStrategy implements Function<ProfileReque
*
* @param strategy the UserInfo response context lookup strategy to use.
*/
- public DefaultUserInfoTokenLookupStrategy(
+ public DefaultUserInfoJWTLookupStrategy(
@Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
userInfoResponseContextLookupStrategy =
Constraint.isNotNull(strategy, "UserInfoResponseContext lookup strategy can not be null");
@@ -70,10 +70,10 @@ public class DefaultUserInfoTokenLookupStrategy implements Function<ProfileReque
final UserInfoResponseContext userInfoContext = userInfoResponseContextLookupStrategy.apply(prc);
if (userInfoContext == null || userInfoContext.getUserInfo() == null ||
- userInfoContext.getUserInfo().getType() != UserInfoResponseType.JWT) {
+ userInfoContext.getUserInfo().getEntityContentType() != ContentType.APPLICATION_JWT) {
return null;
}
- return ((JWTUserInfoResponse)userInfoContext.getUserInfo()).getResponseJwt();
+ return userInfoContext.getUserInfo().getUserInfoJWT();
}
}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/IDTokenInAccessTokenUpdateStrategy.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/IDTokenInAccessTokenUpdateStrategy.java
index 3703e56..ee86219 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/IDTokenInAccessTokenUpdateStrategy.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/IDTokenInAccessTokenUpdateStrategy.java
@@ -29,7 +29,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.nimbusds.jwt.JWT;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import net.minidev.json.JSONObject;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
import net.shibboleth.utilities.java.support.annotation.ParameterName;
import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -69,8 +72,14 @@ public class IDTokenInAccessTokenUpdateStrategy implements BiConsumer<ProfileReq
final AccessTokenResponseContext context =
tokenResponseContextLookupStrategy.apply(profileRequestContext);
- if (context != null) {
- context.setIdToken(idToken);
+ if (context != null) {
+ try {
+ final JSONObject jsonToken = context.getTokenResponse().toJSONObject();
+ jsonToken.put("id_token", idToken.serialize());
+ context.setTokenResponse(OIDCTokenResponse.parse(jsonToken));
+ } catch (final ParseException e) {
+ log.warn("Unable to set id_token back onto access token response", e);
+ }
} else {
log.warn("Unable to set id_token back onto access token response context");
}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/IDTokenJOSEHeaderLookupStrategy.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/IDTokenJOSEHeaderLookupStrategy.java
index fd5f495..e5e5041 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/IDTokenJOSEHeaderLookupStrategy.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/IDTokenJOSEHeaderLookupStrategy.java
@@ -63,10 +63,11 @@ public class IDTokenJOSEHeaderLookupStrategy implements Function<ProfileRequestC
@Override
@Nullable public JWSHeader apply(@Nonnull final ProfileRequestContext prc) {
final AccessTokenResponseContext tokenContext = tokenResponseContextLookupStrategy.apply(prc);
- if (tokenContext == null || tokenContext.getIdToken() == null) {
+ if (tokenContext == null || tokenContext.getTokenResponse() == null ||
+ tokenContext.getTokenResponse().getOIDCTokens().getIDToken() == null) {
return null;
}
- final Header header = tokenContext.getIdToken().getHeader();
+ final Header header = tokenContext.getTokenResponse().getOIDCTokens().getIDToken().getHeader();
if (header instanceof JWSHeader) {
return (JWSHeader) header;
}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/RequestObjectTokenUpdateStrategy.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/RequestObjectTokenUpdateStrategy.java
index f7ce4bb..7a8642a 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/RequestObjectTokenUpdateStrategy.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/RequestObjectTokenUpdateStrategy.java
@@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory;
import com.nimbusds.jwt.JWT;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.navigate.AccessTokenLookupStrategy;
import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -36,7 +37,7 @@ public class RequestObjectTokenUpdateStrategy implements BiConsumer<JWT, Message
/** Class logger. */
@Nonnull
- private final Logger log = LoggerFactory.getLogger(RawTokenResponseLookupStrategy.class);
+ private final Logger log = LoggerFactory.getLogger(AccessTokenLookupStrategy.class);
/** Strategy used to locate the {@link OIDCAuthenticationRequest} to sign. */
@Nonnull private Function<MessageContext, OIDCAuthenticationRequest> authenticationRequestLookupStrategy;
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/SubFromIDTokenLookupFunction.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/SubFromIDTokenLookupFunction.java
index e4a1d95..7c7105f 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/SubFromIDTokenLookupFunction.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/SubFromIDTokenLookupFunction.java
@@ -78,11 +78,11 @@ public class SubFromIDTokenLookupFunction implements BiFunction<ProfileRequestCo
@Nullable
public String apply(@Nonnull final ProfileRequestContext prc, @Nullable final JWTClaimsSet claimsSet) {
final AccessTokenResponseContext tokenContext = tokenResponseContextLookupStrategy.apply(prc);
- if (tokenContext == null || tokenContext.getIdToken() == null) {
+ if (tokenContext == null || tokenContext.getTokenResponse().getOIDCTokens().getIDToken() == null) {
return null;
}
try {
- final JWTClaimsSet claims = tokenContext.getIdToken().getJWTClaimsSet();
+ final JWTClaimsSet claims = tokenContext.getTokenResponse().getOIDCTokens().getIDToken().getJWTClaimsSet();
if (claims.getSubject() != null) {
return claims.getSubject();
}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/TokenResponseIDTokenLookupStrategy.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/TokenResponseIDTokenLookupStrategy.java
deleted file mode 100644
index 0cfba49..0000000
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/TokenResponseIDTokenLookupStrategy.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements. See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You 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.authn.oidc.rp.context.navigate;
-
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.messaging.context.MessageContext;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
-
-/** Return the raw base64 encoded id_token from the TokenResponseContext, or {@code null} if not found.*/
-public class TokenResponseIDTokenLookupStrategy implements Function<ProfileRequestContext, String> {
-
- /** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(TokenResponseIDTokenLookupStrategy.class);
-
- @Override
- @Nullable public String apply(@Nonnull final ProfileRequestContext profileRequestContext) {
-
- final MessageContext inbound = profileRequestContext.getInboundMessageContext();
- if (inbound == null) {
- log.debug("Inbound message context was null, no id_token found");
- return null;
- }
- final AccessTokenResponseContext tokenResponseContext = inbound.getSubcontext(AccessTokenResponseContext.class);
- if (tokenResponseContext == null) {
- log.debug("Token response context was null, no id_token found");
- return null;
- }
- if (tokenResponseContext.getRawTokenResponse() != null) {
- final Object idTokenObject = tokenResponseContext.getRawTokenResponse().get("id_token");
- if (idTokenObject instanceof String) {
- return (String) idTokenObject;
- }
- log.debug("Token response was not a string, no id_token found");
- }
- log.debug("Raw token response was null, no id_token found");
- return null;
-
- }
-
-}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/UserInfoInUserInfoResponseContextUpdateStrategy.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/UserInfoInUserInfoResponseContextUpdateStrategy.java
index d375bb1..7381e6f 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/UserInfoInUserInfoResponseContextUpdateStrategy.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/navigate/UserInfoInUserInfoResponseContextUpdateStrategy.java
@@ -29,11 +29,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.nimbusds.jwt.JWT;
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.JWTUserInfoResponse;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse;
import net.shibboleth.utilities.java.support.annotation.ParameterName;
import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -77,7 +77,8 @@ public class UserInfoInUserInfoResponseContextUpdateStrategy implements BiConsu
final UserInfoResponseContext context =
userInfoResponseContextLookupStrategy.apply(profileRequestContext);
if (context != null) {
- context.setUserInfo(new JWTUserInfoResponse(token));
+ // Create a new UserInfo element with the new JWT response
+ context.setUserInfo(new UserInfoSuccessResponse(token));
} else {
log.warn("Unable to set UserInfo back onto response context");
}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/JWTUserInfoResponse.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/JWTUserInfoResponse.java
deleted file mode 100644
index baba023..0000000
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/JWTUserInfoResponse.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements. See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You 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.authn.oidc.rp.messaging;
-
-import java.text.ParseException;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import com.nimbusds.jwt.EncryptedJWT;
-import com.nimbusds.jwt.JWT;
-import com.nimbusds.jwt.SignedJWT;
-import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
-
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
-/** A UserInfo response returned as a JWT which maybe signed and or encrypted.*/
-public class JWTUserInfoResponse implements UserInfoResponse {
-
- /** The UserInfo claims inside a JWT.*/
- @Nonnull private final JWT responseJwt;
-
- /**
- * The claims represented by the JWT, could be null if JWT is encrypted
- * and has not yet been decrypted.
- */
- @Nullable private ClaimsSet claims;
-
- /**
- *
- * Constructor.
- *
- * @param jwt the plain, signed, and or encrypted JWT representing UserInfo claims.
- */
- public JWTUserInfoResponse(@Nonnull final JWT jwt) {
- responseJwt = Constraint.isNotNull(jwt, "JWT UserInfo response can not be null");
-
- try {
- if (jwt.getJWTClaimsSet() != null) {
- claims = new ClaimsSet();
- claims.putAll(jwt.getJWTClaimsSet().getClaims());
- }
- } catch (final ParseException e) {
- // do nothing, claims are null, likely encrypted JWT.
- claims = null;
- }
-
- }
-
- /**
- * Get the UserInfo JWT response.
- *
- * @return the UserInfo JWT response.
- */
- @Nonnull public JWT getResponseJwt() {
- return responseJwt;
- }
-
- @Override
- @Nullable public ClaimsSet getClaimsSet() {
- return claims;
- }
-
- @Override
- @Nullable public String getSub() {
- if (claims != null) {
- return claims.getStringClaim("sub");
- }
- return null;
- }
-
- @Override
- public boolean isClaimsSetAvailable() {
- return claims != null;
- }
-
- /**
- * {@inheritDoc}
- *
- * There is no easy way to determine if signed when nested inside a JWE, unless first decrypted.
- */
- @Override
- public boolean isSigned() {
- return responseJwt instanceof SignedJWT;
- }
-
- @Override
- public boolean isEncrypted() {
- return responseJwt instanceof EncryptedJWT;
- }
-
- @Override
- public UserInfoResponseType getType() {
- return UserInfoResponseType.JWT;
- }
-
-}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/PlainUserInfoResponse.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/PlainUserInfoResponse.java
deleted file mode 100644
index 9e50da3..0000000
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/PlainUserInfoResponse.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements. See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You 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.authn.oidc.rp.messaging;
-
-import javax.annotation.Nonnull;
-
-import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
-
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
-/** A UserInfo response that was a plain JSON object with claims.*/
-public class PlainUserInfoResponse implements UserInfoResponse {
-
- /** The UserInfo claims inside a JWT.*/
- @Nonnull private final ClaimsSet userInfoClaims;
-
- /**
- *
- * Constructor.
- *
- * @param claims UserInfo claims.
- */
- public PlainUserInfoResponse(@Nonnull final ClaimsSet claims) {
- userInfoClaims = Constraint.isNotNull(claims, "UserInfo claims can not be null");
- }
-
- /**
- * The consented UserInfo claims.
- *
- * @return the claims.
- */
- @Nonnull public ClaimsSet getUserInfoClaims() {
- return userInfoClaims;
- }
-
- @Override
- public ClaimsSet getClaimsSet() {
- return userInfoClaims;
- }
-
- @Override
- public String getSub() {
- return userInfoClaims.getStringClaim("sub");
- }
-
- @Override
- public boolean isClaimsSetAvailable() {
- return true;
- }
-
- @Override
- public boolean isSigned() {
- return false;
- }
-
- @Override
- public boolean isEncrypted() {
- return false;
- }
-
- @Override
- public UserInfoResponseType getType() {
- return UserInfoResponseType.PLAIN;
- }
-
-}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/OIDCRPException.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/ResponseParsingException.java
similarity index 59%
copy from idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/OIDCRPException.java
copy to idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/ResponseParsingException.java
index 32a33e5..12ae734 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/OIDCRPException.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/ResponseParsingException.java
@@ -15,57 +15,53 @@
* limitations under the License.
*/
-package net.shibboleth.idp.plugin.authn.oidc.rp;
+package net.shibboleth.idp.plugin.authn.oidc.rp.messaging;
-import javax.annotation.concurrent.ThreadSafe;
-
-/**
- * An exception to signal a general OIDC RelyingParty error.
- */
- at ThreadSafe
-public class OIDCRPException extends Exception{
+/** An exception that indiciates an error whilst parsing an OIDC/OAuth response.*/
+public class ResponseParsingException extends Exception {
/** Serial UID. */
- private static final long serialVersionUID = -2380145079984333546L;
+ private static final long serialVersionUID = -1380145079985783546L;
/** Constructor. */
- public OIDCRPException() {
+ public ResponseParsingException() {
super();
-
+
}
/**
* Constructor.
*
- * @param message exception message
- * @param cause exception to be wrapped by this one
+ * @param message
+ * exception message
+ * @param cause
+ * exception to be wrapped by this one
*/
- public OIDCRPException(final String message, final Throwable cause) {
+ public ResponseParsingException(final String message, final Throwable cause) {
super(message, cause);
-
+
}
/**
* Constructor.
*
- * @param message exception message
+ * @param message
+ * exception message
*/
- public OIDCRPException(final String message) {
+ public ResponseParsingException(final String message) {
super(message);
-
+
}
/**
* Constructor.
*
- * @param cause exception to be wrapped by this one
+ * @param cause
+ * exception to be wrapped by this one
*/
- public OIDCRPException(final Throwable cause) {
+ public ResponseParsingException(final Throwable cause) {
super(cause);
-
- }
-
-
+ }
}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/UserInfoResponse.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/UserInfoResponse.java
deleted file mode 100644
index 4a39ca9..0000000
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/UserInfoResponse.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements. See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You 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.authn.oidc.rp.messaging;
-
-import javax.annotation.Nullable;
-
-import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
-
-/**
- * A User information response. Contains consent claims about a subject. A response
- * is either a plain JSON object or a JWT.
- */
-public interface UserInfoResponse {
-
- /** Which type of response is this.*/
- public enum UserInfoResponseType {
- /** A plain JSON Object type.*/
- PLAIN,
-
- /** A signed and possibly encrypted JWT type.*/
- JWT
- }
-
- /**
- * Get the claims for the authenticated end-user.
- *
- * @return the claims for the authenticated end-user. Can be {@literal null} if
- * the underlying claims are not available and {@link #isClaimsSetAvailable()} is false
- * e.g. an encrypted JWT which has not yet been decrypted. If {@link #isClaimsSetAvailable()}
- * is true, this should never return {@literal null}.
- */
- @Nullable ClaimsSet getClaimsSet();
-
- /**
- * Get the subject identifier for the authenticated end-user.
- *
- * @return the subject identifier.
- */
- @Nullable String getSub();
-
- /**
- * Is the claims set available for use? if so, {@link #getClaimsSet()} should
- * never return {@literal null}.
- *
- * @return true iff the claims set can be used.
- */
- boolean isClaimsSetAvailable();
-
- /**
- * Is the response signed i.e. a JWS response.
- *
- * @return true iff the response was signed.
- */
- boolean isSigned();
-
- /**
- * Is the response encrypted i.e. a JWE response. Note, it could also
- * be signed.
- *
- * @return true iff the response is encrypted.
- */
- boolean isEncrypted();
-
- /**
- * Get the UserInfo response type.
- *
- * @return the response type, e.g. plain or JWT.
- */
- UserInfoResponseType getType();
-
-}
-
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/UserInfoEncryptedJWTResponseTypeCondition.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/UserInfoEncryptedJWTResponseTypeCondition.java
index 04cfca2..6d626ed 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/UserInfoEncryptedJWTResponseTypeCondition.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/UserInfoEncryptedJWTResponseTypeCondition.java
@@ -21,6 +21,9 @@ import javax.annotation.Nonnull;
import org.opensaml.profile.context.ProfileRequestContext;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jwt.EncryptedJWT;
+
import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
/**
@@ -31,8 +34,11 @@ public class UserInfoEncryptedJWTResponseTypeCondition extends AbstractUserInfoR
@Override
protected boolean doTest(@Nonnull final ProfileRequestContext prc,
@Nonnull final UserInfoResponseContext context) {
- if (context.getUserInfo() == null) {
+ if (context.getUserInfo() == null || context.getUserInfo().getUserInfoJWT() == null ||
+ context.getUserInfo().getUserInfoJWT().getHeader() == null) {
return false;
}
- return context.getUserInfo().isEncrypted();
+ return (JWEAlgorithm.Family.SYMMETRIC.contains(context.getUserInfo().getUserInfoJWT().getHeader()
+ .getAlgorithm()) || JWEAlgorithm.Family.ASYMMETRIC.contains(context.getUserInfo().getUserInfoJWT()
+ .getHeader().getAlgorithm())) && context.getUserInfo().getUserInfoJWT() instanceof EncryptedJWT;
}}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/UserInfoPlainResponseTypeCondition.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/UserInfoPlainResponseTypeCondition.java
index 1c41d3c..bb3174f 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/UserInfoPlainResponseTypeCondition.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/UserInfoPlainResponseTypeCondition.java
@@ -21,8 +21,9 @@ import javax.annotation.Nonnull;
import org.opensaml.profile.context.ProfileRequestContext;
+import com.nimbusds.common.contenttype.ContentType;
+
import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse.UserInfoResponseType;
/**
* Condition that returns true if the UserInfo response was an plain JSON type i.e. not a signed and or encrypted JWT.
@@ -35,5 +36,6 @@ public class UserInfoPlainResponseTypeCondition extends AbstractUserInfoResponse
if (context.getUserInfo() == null) {
return false;
}
- return context.getUserInfo().getType() == UserInfoResponseType.PLAIN;
+ return context.getUserInfo().getEntityContentType() == ContentType.APPLICATION_JSON &&
+ context.getUserInfo().getUserInfo() != null;
}}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/UserInfoSignedJWTResponseTypeCondition.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/UserInfoSignedJWTResponseTypeCondition.java
index d1322b8..72aa0e3 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/UserInfoSignedJWTResponseTypeCondition.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/UserInfoSignedJWTResponseTypeCondition.java
@@ -21,6 +21,9 @@ import javax.annotation.Nonnull;
import org.opensaml.profile.context.ProfileRequestContext;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.SignedJWT;
+
import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
/**
@@ -31,8 +34,10 @@ public class UserInfoSignedJWTResponseTypeCondition extends AbstractUserInfoResp
@Override
protected boolean doTest(@Nonnull final ProfileRequestContext prc,
@Nonnull final UserInfoResponseContext context) {
- if (context.getUserInfo() == null) {
+ if (context.getUserInfo() == null || context.getUserInfo().getUserInfoJWT() == null) {
return false;
}
- return context.getUserInfo().isSigned();
+
+ return JWSAlgorithm.Family.SIGNATURE.contains(context.getUserInfo().getUserInfoJWT().getHeader()
+ .getAlgorithm()) && context.getUserInfo().getUserInfoJWT() instanceof SignedJWT;
}}
diff --git a/idp-oidc-rp-api/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/config/navigate/IDTokenInAccessTokenUpdateStrategyTest.java b/idp-oidc-rp-api/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/config/navigate/IDTokenInAccessTokenUpdateStrategyTest.java
new file mode 100644
index 0000000..6f67a2d
--- /dev/null
+++ b/idp-oidc-rp-api/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/config/navigate/IDTokenInAccessTokenUpdateStrategyTest.java
@@ -0,0 +1,92 @@
+
+package net.shibboleth.idp.plugin.authn.oidc.rp.config.navigate;
+
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+
+import net.minidev.json.JSONObject;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.navigate.IDTokenInAccessTokenUpdateStrategy;
+import net.shibboleth.idp.plugin.authn.oidc.rp.test.TestTokenHelper;
+import net.shibboleth.oidc.security.credential.DefaultClientSecretCredential;
+
+public class IDTokenInAccessTokenUpdateStrategyTest {
+
+ /** The client_secret.*/
+ private static final String CLIENT_SECRET = "Xp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$";
+
+ /** The strategy to test.*/
+ private IDTokenInAccessTokenUpdateStrategy strategy;
+
+
+ /**
+ * Set an encrypted token into the context and update it with a signed token. Leaving all the other tokens in place.
+ *
+ * @throws Exception on error.
+ */
+ @Test
+ public void testUpdateEncryptedToSigned() throws Exception {
+
+ final String tokenResponse =
+ TestTokenHelper.createAccessTokenResponseJSON(Collections.emptyMap(), JWSAlgorithm.HS256,
+ JWEAlgorithm.DIR, EncryptionMethod.A128CBC_HS256,
+ new DefaultClientSecretCredential(CLIENT_SECRET).toSigningCredential(),
+ new DefaultClientSecretCredential(CLIENT_SECRET)
+ .toEncryptionCredential(JWEAlgorithm.DIR, EncryptionMethod.A128CBC_HS256));
+
+ final Map<String, Object> tokenResponseAsMap = new ObjectMapper().readValue(
+ tokenResponse, new TypeReference<Map<String, Object>>() {});
+
+
+ final JWT jwtToUpdate = TestTokenHelper.createJWT(TestTokenHelper.createBasicClaims(Collections.emptyMap()),
+ JWSAlgorithm.HS256, null, null,
+ new DefaultClientSecretCredential(CLIENT_SECRET).toSigningCredential(), null);
+
+ final AccessTokenResponseContext ctx = new AccessTokenResponseContext();
+ try {
+ ctx.setTokenResponse(OIDCTokenResponse.parse(new JSONObject(tokenResponseAsMap)));
+ } catch (final ParseException e) {
+ fail(e.getMessage());
+ }
+ strategy = new IDTokenInAccessTokenUpdateStrategy(prc -> ctx);
+
+ // assert pre-condition the id token is encrypted and the other tokens exist
+ assertNotNull(ctx.getTokenResponse());
+ assertNotNull(ctx.getTokenResponse().getTokens());
+ assertNotNull(ctx.getTokenResponse().getTokens().getAccessToken());
+ assertNotNull(ctx.getTokenResponse().getTokens().getBearerAccessToken());
+ assertNotNull(ctx.getTokenResponse().getOIDCTokens());
+ assertNotNull(ctx.getTokenResponse().getOIDCTokens().getIDToken());
+ assertTrue(ctx.getTokenResponse().getOIDCTokens().getIDToken() instanceof EncryptedJWT);
+
+ strategy.accept(new ProfileRequestContext(), jwtToUpdate);
+
+ // assert post-condition the id token is now signed and the other tokens exist
+ assertNotNull(ctx.getTokenResponse());
+ assertNotNull(ctx.getTokenResponse().getTokens());
+ assertNotNull(ctx.getTokenResponse().getTokens().getAccessToken());
+ assertNotNull(ctx.getTokenResponse().getTokens().getBearerAccessToken());
+ assertNotNull(ctx.getTokenResponse().getOIDCTokens());
+ assertNotNull(ctx.getTokenResponse().getOIDCTokens().getIDToken());
+ assertTrue(ctx.getTokenResponse().getOIDCTokens().getIDToken() instanceof SignedJWT);
+ }
+
+}
diff --git a/idp-oidc-rp-api/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/JWTUserInfoResponseTest.java b/idp-oidc-rp-api/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/JWTUserInfoResponseTest.java
deleted file mode 100644
index db68812..0000000
--- a/idp-oidc-rp-api/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/JWTUserInfoResponseTest.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements. See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You 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.authn.oidc.rp.messaging;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertNull;
-import static org.testng.Assert.assertTrue;
-
-import java.time.Instant;
-import java.util.Date;
-
-import org.testng.annotations.Test;
-
-import com.nimbusds.jose.EncryptionMethod;
-import com.nimbusds.jose.JOSEObjectType;
-import com.nimbusds.jose.JWEAlgorithm;
-import com.nimbusds.jose.JWEHeader;
-import com.nimbusds.jose.JWEObject;
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.JWSHeader;
-import com.nimbusds.jose.Payload;
-import com.nimbusds.jose.crypto.ECDSASigner;
-import com.nimbusds.jose.crypto.RSAEncrypter;
-import com.nimbusds.jose.jwk.Curve;
-import com.nimbusds.jose.jwk.KeyUse;
-import com.nimbusds.jose.jwk.RSAKey;
-import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
-import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
-import com.nimbusds.jwt.EncryptedJWT;
-import com.nimbusds.jwt.JWT;
-import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.jwt.SignedJWT;
-
-/** Tests for JWTUserInfoResponse.*/
-public class JWTUserInfoResponseTest {
-
- @Test
- public void testSignedJwt() throws Exception {
-
- final var key = new ECKeyGenerator(Curve.P_256).keyID("123").generate();
-
- final var header = new JWSHeader.Builder(JWSAlgorithm.ES256)
- .type(JOSEObjectType.JWT)
- .keyID(key.getKeyID())
- .build();
- final var payload = new JWTClaimsSet.Builder()
- .issuer("issuer")
- .audience("rp-proxy")
- .subject("jdoe")
- .expirationTime(Date.from(Instant.now().plusSeconds(120)))
- .build();
-
- final var signedJWT = new SignedJWT(header, payload);
- signedJWT.sign(new ECDSASigner(key.toECPrivateKey()));
-
- final JWTUserInfoResponse response = new JWTUserInfoResponse(signedJWT);
- assertTrue(response.isClaimsSetAvailable());
- assertNotNull(response.getClaimsSet());
- assertEquals(response.getClaimsSet().getStringClaim("sub"), "jdoe");
-
- }
-
- @Test
- public void testEncryptedJwt() throws Exception {
-
- final var keySender = new ECKeyGenerator(Curve.P_256).keyID("1").generate();
- final RSAKey keyRecipient = new RSAKeyGenerator(2048)
- .keyID("2")
- .keyUse(KeyUse.ENCRYPTION)
- .generate();
-
- final var header = new JWSHeader.Builder(JWSAlgorithm.ES256)
- .type(JOSEObjectType.JWT)
- .keyID(keySender.getKeyID())
- .build();
- final var payload = new JWTClaimsSet.Builder()
- .issuer("issuer")
- .audience("rp-proxy")
- .subject("jdoe")
- .expirationTime(Date.from(Instant.now().plusSeconds(120)))
- .build();
-
- final var signedJWT = new SignedJWT(header, payload);
- signedJWT.sign(new ECDSASigner(keySender.toECPrivateKey()));
-
- final JWEObject jweObject =
- new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
- .contentType("JWT")
- .build(),
- new Payload(signedJWT));
- jweObject.encrypt(new RSAEncrypter(keyRecipient.toPublicJWK()));
- final JWT jwt = EncryptedJWT.parse(jweObject.serialize());
-
- final JWTUserInfoResponse response = new JWTUserInfoResponse(jwt);
- assertFalse(response.isClaimsSetAvailable());
- assertNull(response.getClaimsSet());
- assertTrue(response.getResponseJwt().getHeader().getAlgorithm().equals(JWEAlgorithm.RSA_OAEP_256));
-
-
- }
-
-}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TestTokenHelper.java b/idp-oidc-rp-api/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/test/TestTokenHelper.java
similarity index 99%
rename from idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TestTokenHelper.java
rename to idp-oidc-rp-api/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/test/TestTokenHelper.java
index 8bc4547..823a082 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TestTokenHelper.java
+++ b/idp-oidc-rp-api/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/test/TestTokenHelper.java
@@ -16,7 +16,7 @@
*/
-package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
+package net.shibboleth.idp.plugin.authn.oidc.rp.test;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
diff --git a/idp-oidc-rp-impl/pom.xml b/idp-oidc-rp-impl/pom.xml
index fc26154..4ecbdcd 100644
--- a/idp-oidc-rp-impl/pom.xml
+++ b/idp-oidc-rp-impl/pom.xml
@@ -169,6 +169,12 @@
<scope>test</scope>
<type>test-jar</type>
</dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp.plugin.authn</groupId>
+ <artifactId>idp-plugin-oidc-rp-api</artifactId>
+ <scope>test</scope>
+ <type>test-jar</type>
+ </dependency>
<!-- Spring webflow tests require Junit4, runs in TestNG bridge -->
<dependency>
<groupId>junit</groupId>
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultAccessTokenResponseDecoder.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultAccessTokenResponseDecoder.java
new file mode 100644
index 0000000..f744175
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultAccessTokenResponseDecoder.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.authn.oidc.rp.decoding.impl;
+
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.entity.ContentType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.util.MimeType;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.nimbusds.oauth2.sdk.TokenErrorResponse;
+import com.nimbusds.oauth2.sdk.TokenResponse;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+
+import net.minidev.json.JSONObject;
+
+
+/**
+ * Default access token response decoder, which converts a successful HTTP response into an
+ * {@link OIDCTokenResponse}.
+ */
+public class DefaultAccessTokenResponseDecoder extends AbstractJSONResponseDecoderFunction<TokenResponse> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultAccessTokenResponseDecoder.class);
+
+ @Override
+ @Nullable public TokenResponse apply(@Nonnull final HttpResponse httpResponse) {
+
+ try {
+
+ if (httpResponse.getStatusLine() == null) {
+ log.warn("HTTP Response did not contain a status line");
+ return null;
+ }
+
+ final ContentType contentType = ContentType.get(httpResponse.getEntity());
+ if (contentType == null || contentType.getMimeType() == null) {
+ log.warn("HTTP response did not contain a content-type, must contain a content-type");
+ return null;
+ }
+
+ if (MediaType.APPLICATION_JSON.compareTo(MimeType.valueOf(contentType.getMimeType())) != 0) {
+ log.warn("Wrong content type header, expected 'application/json' found '{}'", contentType.getMimeType());
+ return null;
+ }
+
+ final Map<String, Object> tokenResponseAsMap = getObjectMapper().readValue(
+ httpResponse.getEntity().getContent(), new TypeReference<Map<String, Object>>() {});
+
+ final int httpStatusCode = httpResponse.getStatusLine().getStatusCode();
+
+ if (httpStatusCode != HttpStatus.SC_OK) {
+ return TokenErrorResponse.parse(new JSONObject(tokenResponseAsMap));
+ } else if (httpResponse.getEntity() == null || httpResponse.getEntity().getContent() == null) {
+ log.warn("HTTP response does not contain a message entity, nothing to decode, status '{}'",
+ httpStatusCode);
+ return null;
+ }
+ return OIDCTokenResponse.parse(new JSONObject(tokenResponseAsMap));
+
+ } catch (final Exception e) {
+ log.warn("Unable to decode response", e);
+ }
+ return null;
+ }
+
+}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultMapResponseDecoder.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultMapResponseDecoder.java
deleted file mode 100644
index b1963fb..0000000
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultMapResponseDecoder.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements. See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You 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.authn.oidc.rp.decoding.impl;
-
-import java.util.Map;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.nimbusds.jose.util.IOUtils;
-
-/** Default token response decoder, which converts a successful HTTP response into an Map.*/
-public class DefaultMapResponseDecoder extends AbstractJSONResponseDecoderFunction<Map<String, Object>> {
-
- /** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultMapResponseDecoder.class);
-
-
- //TODO we should handle the error response better? just return it and make the decision later?
- @Override
- @Nullable public Map<String, Object> apply(@Nonnull final HttpResponse httpResponse) {
-
- try {
- final int httpStatusCode = httpResponse.getStatusLine().getStatusCode();
- if (httpStatusCode != HttpStatus.SC_OK) {
- log.warn("Non-ok status code ({}) returned from HTTP endpoint", httpStatusCode);
- //dump the body for logging - if one exists
- if (httpResponse.getEntity() != null && httpResponse.getEntity().getContent() != null) {
- final String errorContent = IOUtils.readInputStreamToString(httpResponse.getEntity().getContent());
- log.error("HTTP endpoint returned a Non-ok message of '{}'",errorContent);
- }
- return null;
- } else if (httpResponse.getEntity() == null || httpResponse.getEntity().getContent() == null) {
- log.warn("HTTP response does not contain a message entity, nothing to decode");
- return null;
- }
-
- return getObjectMapper().readValue(httpResponse.getEntity().getContent(),
- new TypeReference<Map<String, Object>>() {});
-
- } catch (final Exception e) {
- log.warn("Unable to decode response", e);
- }
- return null;
- }
-
-}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoder.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoder.java
index ddeb3ad..22e80ca 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoder.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoder.java
@@ -24,23 +24,20 @@ import javax.annotation.Nonnull;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
-import org.apache.http.entity.ContentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.http.MediaType;
-import org.springframework.util.MimeType;
import com.fasterxml.jackson.core.type.TypeReference;
+import com.nimbusds.common.contenttype.ContentType;
import com.nimbusds.jose.util.IOUtils;
-import com.nimbusds.jwt.EncryptedJWT;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTParser;
-import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.UserInfo;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.JWTUserInfoResponse;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.PlainUserInfoResponse;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse;
/**
* Response decoder for UserInfo responses. Supports both plain JSON Object and JWT responses. Importantly,
@@ -51,9 +48,6 @@ import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse;
*/
public class DefaultUserInfoResponseDecoder extends AbstractJSONResponseDecoderFunction<UserInfoResponse> {
- /** The application/jwt media type.*/
- @Nonnull public static final MediaType APPLICATION_JWT = new MediaType("application", "jwt");
-
/** The UserInfo response header that carries error information.*/
@Nonnull public static final String USERINFO_ERROR_RESPONSE_HEADER = "WWW-Authenticate";
@@ -64,76 +58,75 @@ public class DefaultUserInfoResponseDecoder extends AbstractJSONResponseDecoderF
@Override
public UserInfoResponse apply(@Nonnull final HttpResponse httpResponse) {
+ if (httpResponse.getStatusLine() == null) {
+ log.warn("HTTP Response did not contain a status line");
+ return null;
+ }
+
+ final int httpStatusCode = httpResponse.getStatusLine().getStatusCode();
try {
-
- final int httpStatusCode = httpResponse.getStatusLine().getStatusCode();
- if (httpStatusCode != HttpStatus.SC_OK) {
-
- //dump the body for logging - if one exists
- if (httpResponse.getEntity() != null && httpResponse.getEntity().getContent() != null) {
-
- final String errorContent = IOUtils.readInputStreamToString(httpResponse.getEntity().getContent());
- log.error("Non-ok status code ({}) returned from UserInfo HTTP endpoint, error is: '{}' ",
- httpStatusCode, errorContent);
-
- } else if (httpResponse.getHeaders(USERINFO_ERROR_RESPONSE_HEADER) != null &&
+ if (httpStatusCode != HttpStatus.SC_OK) {
+ if (httpResponse.getHeaders(USERINFO_ERROR_RESPONSE_HEADER) != null &&
httpResponse.getHeaders(USERINFO_ERROR_RESPONSE_HEADER).length == 1) {
final Header errorHeader = httpResponse.getHeaders(USERINFO_ERROR_RESPONSE_HEADER)[0];
- log.warn("Non-ok status code ({}) returned from UserInfo HTTP endpoint, error is: '{}'",
- httpStatusCode, errorHeader);
+ if (errorHeader == null) {
+ log.warn("HTTP status code implies error response, but no error given");
+ return null;
+ } else {
+ return UserInfoErrorResponse.parse(errorHeader.getValue());
+ }
+
} else {
- log.warn("Non-ok status code ({}) returned from UserInfo HTTP endpoint", httpStatusCode);
+ log.warn("HTTP status code implies error response, but no error given");
+ return null;
}
- return null;
- } else if (httpResponse.getEntity() == null || httpResponse.getEntity().getContent() == null) {
- log.warn("HTTP response does not contain a message entity, nothing to decode");
- return null;
- }
-
- final ContentType contentType = ContentType.get(httpResponse.getEntity());
- if (contentType == null || contentType.getMimeType() == null) {
- log.warn("HTTP response did not contain a content-type, must contain a content-type");
- return null;
- }
-
- final String content = IOUtils.readInputStreamToString(httpResponse.getEntity().getContent());
-
- // Is a JWT type or plain JSON object
- if (APPLICATION_JWT.compareTo(MimeType.valueOf(contentType.getMimeType())) == 0) {
- // This should fail to parse if plain JSON object type. Although this is less of
- // a concern as parsing a JWT type as a plain object.
- final JWT parsedJwt = JWTParser.parse(content);
-
- if (log.isDebugEnabled()) {
- log.debug("UserInfo response decoder parsed an {} JWT type",
- parsedJwt instanceof SignedJWT ? "Signed" :
- (parsedJwt instanceof EncryptedJWT ? "Encrypted" : "plain"));
+ } else {
+ // Response indicates success
+ if (httpResponse.getEntity() == null || httpResponse.getEntity().getContent() == null) {
+ log.warn("HTTP response did not contain a response entity, nothing to decode");
+ return null;
}
- return new JWTUserInfoResponse(parsedJwt);
+ if (httpResponse.getEntity().getContentType() == null) {
+ log.warn("HTTP response did not contain a content-type, must contain a content-type");
+ return null;
+ }
- } else if (MediaType.APPLICATION_JSON.compareTo(MimeType.valueOf(contentType.getMimeType())) == 0){
+ final ContentType contentType = ContentType.parse(httpResponse.getEntity().getContentType().getValue());
+ if (contentType == null) {
+ log.warn("HTTP response did not contain a valid content-type");
+ return null;
+ }
- // This should fail to parse if the input was a JWT type, but the header was a plain JSON Object type
- final Map<String, Object> claims = getObjectMapper().readValue(
- content, new TypeReference<Map<String, Object>>() {});
- final ClaimsSet claimsSet = new ClaimsSet();
- claimsSet.putAll(claims);
-
- if (log.isTraceEnabled()) {
- log.trace("UserInfo response decoder parsed a plain JSON Object for subject '{}'",
- claimsSet.getStringClaim("sub"));
- }
- return new PlainUserInfoResponse(claimsSet);
- }
+ // Is a JWT type or plain JSON object
+ if (ContentType.APPLICATION_JWT.matches(contentType)) {
+
+ final String content = IOUtils.readInputStreamToString(httpResponse.getEntity().getContent());
+ final JWT parsedJwt = JWTParser.parse(content);
+ return new UserInfoSuccessResponse(parsedJwt);
+
+ } else if (ContentType.APPLICATION_JSON.matches(contentType)){
+
+ final String content = IOUtils.readInputStreamToString(httpResponse.getEntity().getContent());
+ final Map<String, Object> claims = getObjectMapper().readValue(
+ content, new TypeReference<Map<String, Object>>() {});
+ final ClaimsSet claimsSet = new ClaimsSet();
+ claimsSet.putAll(claims);
+ return new UserInfoSuccessResponse(new UserInfo(claimsSet.toJSONObject()));
+ }
+ }
+ } catch (final IllegalArgumentException e) {
+ log.warn("Error creating UserInfo claims set", e);
+ return null;
} catch (final Exception e) {
log.warn("Unable to decode UserInfo response", e);
return null;
}
+ log.warn("Unknown UserInfo response type");
return null;
}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultUserInfoRequestEncoder.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultUserInfoRequestEncoder.java
index a38e8a0..af8889f 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultUserInfoRequestEncoder.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultUserInfoRequestEncoder.java
@@ -33,6 +33,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.nimbusds.jose.util.StandardCharset;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import net.shibboleth.idp.plugin.authn.oidc.rp.OIDCRPException;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
@@ -118,14 +119,14 @@ public class DefaultUserInfoRequestEncoder extends AbstractRequestEncoderFunctio
private void addBearerToken(@Nonnull final RequestBuilder rb,
@Nonnull final AccessTokenResponseContext responseCtx) throws OIDCRPException {
- // Double check is bearer scheme
- if ("Bearer".equals(responseCtx.getRawTokenResponse().get("token_type"))
- && responseCtx.getRawTokenResponse().get("access_token") instanceof String) {
- rb.addHeader("Authorization", "Bearer "+(String)responseCtx.getRawTokenResponse().get("access_token"));
- } else {
- throw new OIDCRPException("Access_token not found, or not Bearer type");
+ if (responseCtx.getTokenResponse() == null) {
+ throw new OIDCRPException("No access token response found");
}
-
+ final BearerAccessToken bearer = responseCtx.getTokenResponse().getTokens().getBearerAccessToken();
+ if (bearer == null) {
+ throw new OIDCRPException("Access token was not Bearer type");
+ }
+ rb.addHeader("Authorization", bearer.toAuthorizationHeader());
}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractHttpOIDCAuthenticationAction.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractHttpOIDCAuthenticationAction.java
index 65fd057..14b34bb 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractHttpOIDCAuthenticationAction.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractHttpOIDCAuthenticationAction.java
@@ -33,6 +33,10 @@ import org.opensaml.security.httpclient.HttpClientSecuritySupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.nimbusds.oauth2.sdk.ErrorObject;
+import com.nimbusds.oauth2.sdk.ErrorResponse;
+import com.nimbusds.oauth2.sdk.Response;
+
import net.shibboleth.idp.plugin.authn.oidc.rp.OIDCRPException;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.AbstractAuthenticatableOIDCContext;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
@@ -42,12 +46,13 @@ import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
/**
- * An abstract class for OIDC actions that make synchronous HTTP requests.
+ * An abstract class for OIDC actions that make synchronous HTTP requests and return {@link Response responses}.
*
* @param <T> the response type of the object returned as a result of the request.
*/
@ThreadSafeAfterInit
-public abstract class AbstractHttpOIDCAuthenticationAction<T> extends AbstractOIDCAuthenticationResponseAction {
+public abstract class AbstractHttpOIDCAuthenticationAction<T extends Response>
+ extends AbstractOIDCAuthenticationResponseAction {
/** Class logger.*/
@Nonnull private final Logger log = LoggerFactory.getLogger(AbstractHttpOIDCAuthenticationAction.class);
@@ -149,15 +154,16 @@ public abstract class AbstractHttpOIDCAuthenticationAction<T> extends AbstractOI
/**
* Encode the request using the supplied request encoder strategy. Execute a synchronous HTTP request
- * and decode the response using the supplied decoder strategy.
+ * and decode the response using the supplied decoder strategy. If an error response is returned an exception
+ * is thrown —the error object is not propagated back to the caller.
*
* @param profileRequestContext the context to pull information out of for encoding the request.
* @param authenticatableContext an authenticatable context to set the authenticated flag. Can be {@literal null} if
* no flag is supplied.
*
- * @return the decoded response.
+ * @return a successful decoded response.
*
- * @throws OIDCRPException on error making the request.
+ * @throws OIDCRPException on error making the request, or if an error response is returned.
*/
@Nonnull protected T handleRequest(@Nonnull final ProfileRequestContext profileRequestContext,
@Nullable final AbstractAuthenticatableOIDCContext authenticatableContext) throws OIDCRPException {
@@ -169,8 +175,9 @@ public abstract class AbstractHttpOIDCAuthenticationAction<T> extends AbstractOI
final HttpResponse response = executeHttpRequest(request, authenticatableContext);
final T responseObject = getHttpResponseDecoderStrategy().apply(response);
if (responseObject == null) {
- throw new OIDCRPException(
- "Unable to process HTTP response, likely error response");
+ throw new OIDCRPException("Unable to process HTTP response");
+ } else if (!responseObject.indicatesSuccess()) {
+ throw new OIDCRPException(formatErrorResponse(((ErrorResponse)responseObject).getErrorObject()));
}
return responseObject;
} catch (final IOException e) {
@@ -178,6 +185,22 @@ public abstract class AbstractHttpOIDCAuthenticationAction<T> extends AbstractOI
throw new OIDCRPException(e);
}
}
+
+ /**
+ * Format an error message string from the {@link ErrorObject} provided.
+ *
+ * @param error the error object
+ *
+ * @return a human readable error string
+ */
+ private String formatErrorResponse(@Nonnull final ErrorObject error) {
+ final StringBuilder errorMsg = new StringBuilder();
+ errorMsg.append("Error response, HTTP status code '")
+ .append(error.getHTTPStatusCode())
+ .append("', error code '").append(error.getCode())
+ .append("', description: ").append(error.getDescription());
+ return errorMsg.toString();
+ }
/**
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForAccessToken.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForAccessToken.java
index 32bbb7a..a18dd80 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForAccessToken.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForAccessToken.java
@@ -17,7 +17,6 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
-import java.util.Map;
import java.util.function.Function;
import javax.annotation.Nonnull;
@@ -30,6 +29,9 @@ import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.nimbusds.oauth2.sdk.TokenResponse;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.OIDCRPException;
@@ -49,7 +51,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
* @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null</pre>
* @post Add the Access Token Response to the {@link AccessTokenResponseContext}.
*/
-public class ExchangeCodeForAccessToken extends AbstractHttpOIDCAuthenticationAction<Map<String, Object>> {
+public class ExchangeCodeForAccessToken extends AbstractHttpOIDCAuthenticationAction<TokenResponse> {
/** Class logger.*/
@Nonnull private final Logger log = LoggerFactory.getLogger(ExchangeCodeForAccessToken.class);
@@ -94,14 +96,18 @@ public class ExchangeCodeForAccessToken extends AbstractHttpOIDCAuthenticationAc
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return;
}
-
try {
- final Map<String, Object> responseObject = handleRequest(profileRequestContext, responseCtx);
- responseCtx.setRawTokenResponse(responseObject);
- log.trace("{}: Token request response '{}'",getLogPrefix(), responseObject);
-
+ final TokenResponse responseObject = handleRequest(profileRequestContext, responseCtx);
+ if (responseObject instanceof OIDCTokenResponse) {
+ responseCtx.setTokenResponse((OIDCTokenResponse)responseObject);
+ log.trace("{} Token request response was successful '{}'",getLogPrefix(),
+ ((OIDCTokenResponse)responseObject).indicatesSuccess());
+ } else {
+ throw new OIDCRPException("Token response was not of the expected format, expected OIDC token "
+ + "response, got " + responseObject.getClass().getSimpleName());
+ }
} catch (final OIDCRPException e) {
- log.error("{} Failed to exchange authorisation code for token result: {}",getLogPrefix(), e.getMessage());
+ log.error("{} Failed to exchange authorisation code for token result",getLogPrefix(), e);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
}
}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponse.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponse.java
deleted file mode 100644
index 8a62c6e..0000000
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponse.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements. See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You 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.authn.oidc.rp.impl;
-
-import java.text.ParseException;
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-
-import org.opensaml.messaging.context.navigate.ChildContextLookup;
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.action.EventIds;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.nimbusds.jose.JOSEObject;
-import com.nimbusds.jose.JWEObject;
-import com.nimbusds.jose.JWSObject;
-import com.nimbusds.jose.PlainObject;
-import com.nimbusds.jwt.EncryptedJWT;
-import com.nimbusds.jwt.SignedJWT;
-
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
-import net.shibboleth.idp.profile.AbstractProfileAction;
-import net.shibboleth.oidc.profile.core.OidcEventIds;
-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;
-
-/**
- * Action that extracts an id_token from the access token response and sets it onto the
- * {@link AccessTokenResponseContext}. The id_token must either be signed, or signed and encrypted,
- * plain id_tokens are not supported — this helps prevent 'alg=none' header manipulation.
- *
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link EventIds#INVALID_PROFILE_CTX}
- * @event {@link OidcEventIds#INVALID_ID_TOKEN}
- * @post id_token added to {@link AccessTokenResponseContext}.
- */
-public class ExtractIDTokenFromResponse extends AbstractProfileAction {
-
- /** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractIDTokenFromResponse.class);
-
- /** Function that looks up a raw id_token value as a base64 encoded string.*/
- @NonnullAfterInit private Function<ProfileRequestContext, String> rawIdTokenLookupStrategy;
-
- /** The token response context to add the decoded id_token too.*/
- @NonnullAfterInit private AccessTokenResponseContext responseCtx;
-
- /** Strategy used to look up the {@link AccessTokenResponseContext} to set the parameters for. */
- @Nonnull private Function<ProfileRequestContext, AccessTokenResponseContext>
- tokenResponseContextLookupStrategy;
-
- /** Constructor.*/
- public ExtractIDTokenFromResponse() {
- tokenResponseContextLookupStrategy =
- new ChildContextLookup<>(AccessTokenResponseContext.class).compose(
- new InboundMessageContextLookup());
- }
-
- /**
- * Set the strategy used to look up a {@link AccessTokenResponseContext}.
- *
- * @param strategy lookup strategy
- */
- public void setTokenResponseContextLookupStrategy(
- @Nonnull final Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
- tokenResponseContextLookupStrategy = Constraint.isNotNull(strategy,
- "TokenResponseContext lookup strategy cannot be null");
- }
-
- @Override
- protected void doInitialize() throws ComponentInitializationException {
- super.doInitialize();
-
- if (rawIdTokenLookupStrategy == null) {
- throw new ComponentInitializationException("RawIdTokenLookupStrategy cannot be null");
- }
- }
-
- @Override
- protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-
- responseCtx = tokenResponseContextLookupStrategy.apply(profileRequestContext);
- if (responseCtx == null) {
- log.debug("{} No TokenResponseContext returned by lookup strategy", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
- return false;
- }
- return true;
- }
-
- /**
- * Set the strategy used to lookup a base64 encoded JWT from the profile request context.
- *
- * @param strategy the strategy to use.
- */
- public void setRawIdTokenLookupStrategy(
- @Nonnull final Function<ProfileRequestContext, String> strategy) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
- rawIdTokenLookupStrategy = Constraint.isNotNull(strategy,"Raw id_token lookup strategy can not be null");
- }
-
- @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-
- final String rawIdTokenValue = rawIdTokenLookupStrategy.apply(profileRequestContext);
- if (rawIdTokenValue == null) {
- log.warn("{} Unable to extract id_token from access token response",getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext,OidcEventIds.INVALID_ID_TOKEN);
- return;
- }
- log.trace("{} Encoded id_token is: '{}'",getLogPrefix(), rawIdTokenValue);
- try {
- final JOSEObject joseObject = JOSEObject.parse(rawIdTokenValue);
- if (joseObject instanceof PlainObject) {
- log.error("{} Plain id_token not supported", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext,OidcEventIds.INVALID_ID_TOKEN);
- return;
-
- } else if (joseObject instanceof JWSObject) {
- log.trace("{} Signed id_token found", getLogPrefix());
- responseCtx.setIdToken(SignedJWT.parse(rawIdTokenValue));
-
- } else if (joseObject instanceof JWEObject) {
- log.trace("{} Encrypted id_token found", getLogPrefix());
- responseCtx.setIdToken(EncryptedJWT.parse(rawIdTokenValue));
- }
- } catch (final ParseException e) {
- log.warn("{} Unable to parse id_token",getLogPrefix(), e);
- ActionSupport.buildEvent(profileRequestContext,OidcEventIds.INVALID_ID_TOKEN);
- return;
- }
- }
-}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ProcessEndUserClaims.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ProcessEndUserClaims.java
index 2b04d9e..cbb760c 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ProcessEndUserClaims.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ProcessEndUserClaims.java
@@ -33,6 +33,7 @@ import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.nimbusds.common.contenttype.ContentType;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
@@ -206,24 +207,37 @@ public class ProcessEndUserClaims extends AbstractOIDCAuthenticationResponseActi
log.trace("{} No UserInfo response context returned by lookup strategy, creating empty "
+ "UserInfo claims", getLogPrefix());
userInfoClaims = new ClaimsSet();
- } else{
- userInfoClaims = userInfoCtx.getUserInfo().getClaimsSet();
+ } else if (userInfoCtx.getUserInfo().getEntityContentType() == ContentType.APPLICATION_JSON) {
+ userInfoClaims = userInfoCtx.getUserInfo().getUserInfo();
+ } else if (userInfoCtx.getUserInfo().getEntityContentType() == ContentType.APPLICATION_JWT) {
+ try {
+ final ClaimsSet claims = new ClaimsSet();
+ claims.putAll(userInfoCtx.getUserInfo().getUserInfoJWT().getJWTClaimsSet().getClaims());
+ userInfoClaims = claims;
+ } catch (final ParseException e) {
+ log.warn("Unable to extract UserInfo claims from JWT claimsset", e);
+ return false;
+ }
+ } else {
+ log.warn("Unable to extract UserInfo claims, unknown entity content type");
+ return false;
}
final AccessTokenResponseContext tokenResponseCtx =
accessTokenResponseContextLookupStrategy.apply(profileRequestContext);
- if (tokenResponseCtx == null) {
- log.debug("{} No AccessTokenResponseContext returned by lookup strategy", getLogPrefix());
+ if (tokenResponseCtx == null || tokenResponseCtx.getTokenResponse() == null) {
+ log.debug("{} No AccessTokenResponseContext or Access Token returned by lookup strategy", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return false;
}
- if (tokenResponseCtx.getIdToken() == null) {
+ if (tokenResponseCtx.getTokenResponse().getOIDCTokens() == null ||
+ tokenResponseCtx.getTokenResponse().getOIDCTokens().getIDToken() == null) {
log.debug("{} AccessTokenResponseContext did not contain an id_token", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return false;
}
try {
- idTokenClaims = tokenResponseCtx.getIdToken().getJWTClaimsSet();
+ idTokenClaims = tokenResponseCtx.getTokenResponse().getOIDCTokens().getIDToken().getJWTClaimsSet();
if (idTokenClaims == null) {
log.debug("{} AccessTokenResponseContext did not contain an id_token with accessible claims, "
+ "possibly still encrypted",
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/UserInfoEndpointLookup.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/UserInfoEndpointLookup.java
index d6d36e2..246ae81 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/UserInfoEndpointLookup.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/UserInfoEndpointLookup.java
@@ -29,12 +29,14 @@ import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.OIDCRPException;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -91,11 +93,14 @@ public class UserInfoEndpointLookup extends AbstractHttpOIDCAuthenticationAction
return;
}
- try {
- userInfoCtx.setUserInfo(handleRequest(profileRequestContext, userInfoCtx));
+ try {
+ final UserInfoResponse response = handleRequest(profileRequestContext, userInfoCtx);
+ if (response.indicatesSuccess()) {
+ userInfoCtx.setUserInfo(response.toSuccessResponse());
+ }
} catch (final OIDCRPException e) {
log.error("{} Unable to return claims from UserInfo endpoint",getLogPrefix(),e);
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
}
}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOAuthAccessTokenResponse.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOAuthAccessTokenResponse.java
index 6b820ad..0a19967 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOAuthAccessTokenResponse.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOAuthAccessTokenResponse.java
@@ -17,7 +17,8 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
-import java.util.Map;
+import java.time.Duration;
+import java.time.Instant;
import java.util.function.Function;
import javax.annotation.Nonnull;
@@ -30,6 +31,8 @@ import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
import net.shibboleth.oidc.profile.core.OidcEventIds;
@@ -41,6 +44,8 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
* Validation action that validates the OAuth Access Token Response against RFC 6749 section 5.1
* and OpenID Connect Core 1.0 section 3.1.3.3.
*
+ * <p>Validation will also be performed in the response decoders when creating the access token.</p>
+ *
* @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
* @event {@link EventIds#INVALID_PROFILE_CTX}
* @event {@link OidcEventIds#INVALID_ACCESS_TOKEN}
@@ -84,53 +89,48 @@ public class ValidateOAuthAccessTokenResponse extends AbstractOIDCAuthentication
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return;
}
- final Map<String, Object> rawTokenResponse = responseCtx.getRawTokenResponse();
- if (rawTokenResponse == null) {
- log.debug("{} No access token response found, response invalid", getLogPrefix());
+ final OIDCTokenResponse tokenResponse = responseCtx.getTokenResponse();
+ if (tokenResponse == null) {
+ log.warn("{} No Access Token response found, response invalid", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ACCESS_TOKEN);
return;
}
// Look for an error response. This may never get here depending on the upflow response decoder used.
- if (rawTokenResponse.containsKey("error")) {
- log.debug("{} Error response found instead of access token: '{}'", getLogPrefix(),
- rawTokenResponse.get("error"));
+ if (!tokenResponse.indicatesSuccess()) {
+ if (log.isWarnEnabled()) {
+ log.warn("{} Error response found instead of access token response", getLogPrefix());
+ }
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ACCESS_TOKEN);
return;
}
- if (!rawTokenResponse.containsKey("id_token")) {
+
+ if (tokenResponse.getOIDCTokens().getIDToken() == null) {
log.warn("{} Access token response is invalid, no id_token found", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ACCESS_TOKEN);
return;
}
// Otherwise check is valid success response
- if (!rawTokenResponse.containsKey("access_token")) {
+ if (tokenResponse.getTokens().getAccessToken() == null) {
log.warn("{} Access token response is invalid, no access_token found", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ACCESS_TOKEN);
return;
}
- if (!rawTokenResponse.containsKey("token_type")) {
- log.warn("{} Access token response is invalid, no token_type found", getLogPrefix());
+ if (tokenResponse.getTokens().getBearerAccessToken() == null) {
+ log.warn("{} Access token response is invalid, bearer token_type required", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ACCESS_TOKEN);
return;
- } else {
- // Only support Bearer type
- final Object tokenTypeObject = rawTokenResponse.get("token_type");
- if (tokenTypeObject instanceof String) {
- if (!"Bearer".equals(tokenTypeObject)) {
- log.warn("{} Access token response is invalid, bearer token_type required", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ACCESS_TOKEN);
- return;
- }
- } else {
- log.warn("{} Access token response is invalid, bearer token_type required", getLogPrefix());
+ }
+ if (tokenResponse.getTokens().getAccessToken().getLifetime() != 0 &&
+ responseCtx.getTokenResponseCreatedAt() != null) {
+ final Instant now = Instant.now();
+ final Instant expiresAt = responseCtx.getTokenResponseCreatedAt().plus(Duration.ofSeconds(
+ tokenResponse.getTokens().getAccessToken().getLifetime()));
+ if (expiresAt.isBefore(now)) {
+ log.warn("{} Access token response is invalid, token has expired", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ACCESS_TOKEN);
return;
}
}
- // Is recommended, but we do not use it
- if (!rawTokenResponse.containsKey("expires_in")) {
- log.trace("{} Expires_in not found in access token, is recomended ", getLogPrefix());
- }
}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoJSONObjectClaims.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoJSONObjectClaims.java
index 1f22a5a..ec4e78a 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoJSONObjectClaims.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoJSONObjectClaims.java
@@ -31,13 +31,16 @@ import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.nimbusds.common.contenttype.ContentType;
import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
import net.shibboleth.idp.authn.AbstractAuthenticationAction;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse;
import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -122,13 +125,13 @@ public class ValidateUserInfoJSONObjectClaims extends AbstractAuthenticationActi
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return false;
}
- if (tokenResponseCtx.getIdToken() == null) {
+ if (tokenResponseCtx.getTokenResponse().getOIDCTokens().getIDToken() == null) {
log.debug("{} AccessTokenResponseContext did not contain an id_token", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return false;
}
try {
- idTokenClaims = tokenResponseCtx.getIdToken().getJWTClaimsSet();
+ idTokenClaims = tokenResponseCtx.getTokenResponse().getOIDCTokens().getIDToken().getJWTClaimsSet();
if (idTokenClaims == null) {
log.debug("{} AccessTokenResponseContext did not contain an id_token with accessible claims, "
+ "possibly still encrypted",
@@ -148,14 +151,14 @@ public class ValidateUserInfoJSONObjectClaims extends AbstractAuthenticationActi
@Nonnull final AuthenticationContext authenticationContext) {
log.trace("{} Validating UserInfo JSON Object claims", getLogPrefix());
-
- final UserInfoResponse response = userInfoCtx.getUserInfo();
- if (!response.isClaimsSetAvailable()) {
- log.debug("{} UserInfo claims are not available, check response is not still encrypted", getLogPrefix());
+
+ final ClaimsSet claims = resolveClaimsSet(userInfoCtx.getUserInfo());
+ if (claims == null) {
+ log.debug("{} UserInfo claims can not be resolved", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
return;
}
- final String subFromUserInfo = response.getClaimsSet().getStringClaim("sub");
+ final String subFromUserInfo = claims.getStringClaim("sub");
if (subFromUserInfo == null) {
log.debug("{} UserInfo claims does not contain the 'sub' claim, it must", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
@@ -163,14 +166,50 @@ public class ValidateUserInfoJSONObjectClaims extends AbstractAuthenticationActi
}
// sub must match to id_token sub
if (!idTokenClaims.getSubject().equals(subFromUserInfo)){
- log.debug("{} UserInfo claims about subject '{}' but id_token about subject '{}', mismatch",
+ log.error("{} UserInfo claims about subject '{}' but id_token about subject '{}', mismatch",
getLogPrefix(), subFromUserInfo, idTokenClaims.getSubject());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
return;
}
- log.debug("{} UserInfo claims are valid for '{}'", getLogPrefix(),
- response.getClaimsSet().getStringClaim("sub"));
+ log.debug("{} UserInfo claims are valid for '{}'", getLogPrefix(), claims.getStringClaim("sub"));
}
+
+ /**
+ * Resolve the UserInfo claims from the user info response. If JWT type, extract from the JWT claims. If plain JSON
+ * object type, resolve directly from the UserInfo claims set.
+ *
+ * @param response the UserInfo response
+ *
+ * @return the UserInfo claims. Return {@code null} if not found, wrong response type, or there was a parsing exception
+ */
+ @Nullable private ClaimsSet resolveClaimsSet(@Nonnull final UserInfoResponse response) {
+ if (!response.indicatesSuccess()) {
+ return null;
+ }
+ final UserInfoSuccessResponse successResponse = response.toSuccessResponse();
+ if (successResponse.getEntityContentType() == ContentType.APPLICATION_JWT) {
+ try {
+ final JWTClaimsSet claims = successResponse.getUserInfoJWT().getJWTClaimsSet();
+ if (claims == null) {
+ log.debug("{} UserInfo claims are not available, check response is not still encrypted",
+ getLogPrefix());
+ return null;
+ }
+ final ClaimsSet claimsConverted = new ClaimsSet();
+ claimsConverted.putAll(claims.toJSONObject());
+ return claimsConverted;
+ } catch (final ParseException e) {
+ log.warn("{} UserInfo claims could not be extracted from the JWT claims set", getLogPrefix(), e);
+ return null;
+ }
+
+ } else if (successResponse.getEntityContentType() == ContentType.APPLICATION_JSON) {
+ return successResponse.getUserInfo();
+ } else {
+ log.debug("{} UserInfo claims are not available, unknown entity content type", getLogPrefix());
+ return null;
+ }
+ }
}
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
index 2295310..cd3c474 100644
--- a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
@@ -384,17 +384,9 @@
p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup" />
- <bean id="ExtractIDTokenFromTokenResponse" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ExtractIDTokenFromResponse"
- p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
- p:rawIdTokenLookupStrategy-ref="TokenResponseIDTokenLookupStrategy" />
-
- <bean id="TokenResponseIDTokenLookupStrategy"
- class="net.shibboleth.idp.plugin.authn.oidc.rp.context.navigate.TokenResponseIDTokenLookupStrategy" />
-
<!-- could these be singletons? -->
<bean id="shibboleth.authn.oidc.rp.DefaultTokenResponseDecoder" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.oidc.rp.decoding.impl.DefaultMapResponseDecoder"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.decoding.impl.DefaultAccessTokenResponseDecoder"
p:objectMapper-ref="shibboleth.authn.oidc.rp.JSONObjectMapper" />
<bean id="shibboleth.authn.oidc.rp.DefaultAuthCodeTokenResponseEncoder" scope="prototype"
@@ -449,7 +441,7 @@
<bean class="net.shibboleth.idp.profile.context.navigate.SpringExpressionContextLookupFunction"
c:_0="#{ T(org.opensaml.profile.context.ProfileRequestContext) }"
c:outputType="#{T(com.nimbusds.jwt.EncryptedJWT)}"
- c:expression="#input.getInboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext)).getIdToken()" />
+ c:expression="#input.getInboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext)).getTokenResponse().getOIDCTokens().getIDToken()" />
</property>
<property name="jwtUpdateStrategy">
<bean class="net.shibboleth.idp.plugin.authn.oidc.rp.context.navigate.IDTokenInAccessTokenUpdateStrategy" />
@@ -493,7 +485,7 @@
class="net.shibboleth.idp.profile.context.navigate.SpringExpressionContextLookupFunction"
c:_0="#{ T(org.opensaml.messaging.context.MessageContext) }"
c:outputType="#{T(com.nimbusds.jwt.SignedJWT)}"
- c:expression="#input.getSubcontext(T(net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext)).getIdToken()" />
+ c:expression="#input.getSubcontext(T(net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext)).getTokenResponse().getOIDCTokens().getIDToken()" />
</property>
<property name="providerMetadataLookupStrategy">
<ref bean="shibboleth.ChildLookup.OIDCProviderMetadataFromPeerEntityContext" />
@@ -638,13 +630,13 @@
<bean id="AtHashValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.AccessTokenHashValidator"
p:allowMissing="%{idp.authn.oidc.rp.client.tokenresponse.allowMissingAtHash:true}"
- p:tokenResponseLookupStrategy="#{getObject('shibboleth.authn.oidc.rp.jwt.TokenResponseLookupStrategy') ?:
- getObject('shibboleth.authn.oidc.rp.jwt.DefaultTokenResponseLookupStrategy')}"
+ p:accessTokenLookupStrategy="#{getObject('shibboleth.authn.oidc.rp.jwt.AccessTokenLookupStrategy') ?:
+ getObject('shibboleth.authn.oidc.rp.jwt.DefaultAccessTokenLookupStrategy')}"
p:joseHeaderLookupStrategy="#{getObject('shibboleth.authn.oidc.rp.jwt.IDTokenJOSEHeaderLookupStrategy') ?:
getObject('shibboleth.authn.oidc.rp.jwt.DefaultIDTokenJOSEHeaderLookupStrategy')}"/>
- <bean id="shibboleth.authn.oidc.rp.jwt.DefaultTokenResponseLookupStrategy"
- class="net.shibboleth.idp.plugin.authn.oidc.rp.context.navigate.RawTokenResponseLookupStrategy"/>
+ <bean id="shibboleth.authn.oidc.rp.jwt.DefaultAccessTokenLookupStrategy"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.context.navigate.AccessTokenLookupStrategy"/>
<bean id="shibboleth.authn.oidc.rp.jwt.DefaultIDTokenJOSEHeaderLookupStrategy"
class="net.shibboleth.idp.plugin.authn.oidc.rp.context.navigate.IDTokenJOSEHeaderLookupStrategy"/>
@@ -719,7 +711,7 @@
<bean class="net.shibboleth.idp.profile.context.navigate.SpringExpressionContextLookupFunction"
c:_0="#{ T(org.opensaml.profile.context.ProfileRequestContext) }"
c:outputType="#{T(com.nimbusds.jwt.EncryptedJWT)}"
- c:expression="#input.getInboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext)).getUserInfo().getResponseJwt()" />
+ c:expression="#input.getInboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext)).getUserInfo().getUserInfoJWT()" />
</property>
<property name="jwtUpdateStrategy">
<bean class="net.shibboleth.idp.plugin.authn.oidc.rp.context.navigate.UserInfoInUserInfoResponseContextUpdateStrategy" />
@@ -765,7 +757,7 @@
class="net.shibboleth.idp.profile.context.navigate.SpringExpressionContextLookupFunction"
c:_0="#{ T(org.opensaml.messaging.context.MessageContext) }"
c:outputType="#{T(com.nimbusds.jwt.SignedJWT)}"
- c:expression="#input.getSubcontext(T(net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext)).getUserInfo().getResponseJwt()" />
+ c:expression="#input.getSubcontext(T(net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext)).getUserInfo().getUserInfoJWT()" />
</property>
<property name="providerMetadataLookupStrategy">
<ref bean="shibboleth.ChildLookup.OIDCProviderMetadataFromPeerEntityContext" />
@@ -782,7 +774,7 @@
</bean>
<bean id="shibboleth.authn.oidc.rp.DefaultUserInfoTokenLookupStrategy"
- class="net.shibboleth.idp.plugin.authn.oidc.rp.context.navigate.DefaultUserInfoTokenLookupStrategy" />
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.context.navigate.DefaultUserInfoJWTLookupStrategy" />
<bean id="ValidateUserInfoTokenClaims" scope="prototype"
class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ValidateTokenClaims"
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
index 0344758..29aa18d 100644
--- a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
@@ -115,7 +115,6 @@
<evaluate expression="InitializeOAuth2ClientAuthenticationContext" />
<evaluate expression="ExchangeCodeForAccessToken" />
<evaluate expression="ValidateOAuthAccessTokenResponse" />
- <evaluate expression="ExtractIDTokenFromTokenResponse" />
<evaluate expression="PopulateIDTokenDecryptionParameters" />
<evaluate expression="DecryptIDTokenJWE" />
<!--Validation of the JWT signature is optional if TLS server validation was performed -->
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultMapResponseDecoderTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultAccessTokenResponseDecoderTest.java
similarity index 62%
rename from idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultMapResponseDecoderTest.java
rename to idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultAccessTokenResponseDecoderTest.java
index 7a81c8c..5f537ae 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultMapResponseDecoderTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultAccessTokenResponseDecoderTest.java
@@ -18,35 +18,42 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.decoding.impl;
import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
-import java.io.ByteArrayInputStream;
import java.io.IOException;
-import java.util.Map;
-import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
import org.mockito.Mockito;
+import org.springframework.http.MediaType;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.oauth2.sdk.AccessTokenResponse;
+import com.nimbusds.oauth2.sdk.ErrorResponse;
+import com.nimbusds.oauth2.sdk.TokenResponse;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
import net.shibboleth.idp.plugin.authn.oidc.rp.impl.AbstractOIDCTest;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-/** Tests for the DefaultTokenResponseDecoder.*/
-public class DefaultMapResponseDecoderTest extends AbstractOIDCTest {
+/** Tests for the DefaultAccessTokenResponseDecoder.*/
+public class DefaultAccessTokenResponseDecoderTest extends AbstractOIDCTest {
/** The encoder to test.*/
- private DefaultMapResponseDecoder decoder;
+ private DefaultAccessTokenResponseDecoder decoder;
+ @Override
@BeforeMethod
public void setup() throws Exception {
super.setup();
- decoder = new DefaultMapResponseDecoder();
+ decoder = new DefaultAccessTokenResponseDecoder();
decoder.setObjectMapper(new ObjectMapper());
}
@@ -55,18 +62,20 @@ public class DefaultMapResponseDecoderTest extends AbstractOIDCTest {
public void testEncoder_Success() throws ComponentInitializationException, UnsupportedOperationException, IOException {
decoder.initialize();
final HttpResponse response = Mockito.mock(HttpResponse.class);
- final HttpEntity mockHttpEntity = Mockito.mock(HttpEntity.class);
final StatusLine mockStatusLine = Mockito.mock(StatusLine.class);
when(mockStatusLine.getStatusCode()).thenReturn(200);
when(response.getStatusLine()).thenReturn(mockStatusLine);
- when(response.getEntity()).thenReturn(mockHttpEntity);
- when(mockHttpEntity.getContent()).thenReturn(new ByteArrayInputStream(ACCESS_TOKEN_RESPONSE.getBytes()));
- final Map<String, Object> decodedResponse = decoder.apply(response);
+ Mockito.when(response.getEntity()).thenReturn(new StringEntity(ACCESS_TOKEN_RESPONSE,
+ ContentType.parse(MediaType.APPLICATION_JSON_VALUE)));
+
+ final TokenResponse decodedResponse = decoder.apply(response);
assertNotNull(decodedResponse);
- assertNotNull(decodedResponse.get("id_token"));
- assertNotNull(decodedResponse.get("access_token"));
- assertNotNull(decodedResponse.get("token_type"));
- assertNotNull(decodedResponse.get("scope"));
+ assertTrue(decodedResponse.indicatesSuccess());
+ final AccessTokenResponse tokenResponse = decodedResponse.toSuccessResponse();
+ assertTrue(tokenResponse instanceof OIDCTokenResponse);
+ assertNotNull(((OIDCTokenResponse)tokenResponse).getTokens().getAccessToken());
+ assertNotNull(((OIDCTokenResponse)tokenResponse).getTokens().getBearerAccessToken());
+ assertNotNull(((OIDCTokenResponse)tokenResponse).getOIDCTokens().getIDToken());
}
@Test
@@ -74,9 +83,8 @@ public class DefaultMapResponseDecoderTest extends AbstractOIDCTest {
decoder.initialize();
final HttpResponse response = Mockito.mock(HttpResponse.class);
- final Map<String, Object> decodedResponse = decoder.apply(response);
+ final TokenResponse decodedResponse = decoder.apply(response);
assertNull(decodedResponse);
-
}
@Test
@@ -86,10 +94,15 @@ public class DefaultMapResponseDecoderTest extends AbstractOIDCTest {
final StatusLine mockStatusLine = Mockito.mock(StatusLine.class);
when(mockStatusLine.getStatusCode()).thenReturn(500);
when(response.getStatusLine()).thenReturn(mockStatusLine);
+ Mockito.when(response.getEntity()).thenReturn(new StringEntity(TOKEN_RESPONSE_ERROR,
+ ContentType.parse(MediaType.APPLICATION_JSON_VALUE)));
- final Map<String, Object> decodedResponse = decoder.apply(response);
- assertNull(decodedResponse);
+ final TokenResponse decodedResponse = decoder.apply(response);
+ assertNotNull(decodedResponse);
+ assertFalse(decodedResponse.indicatesSuccess());
+ final ErrorResponse error = decodedResponse.toErrorResponse();
+ assertNotNull(error.getErrorObject());
}
@Test
@@ -101,7 +114,7 @@ public class DefaultMapResponseDecoderTest extends AbstractOIDCTest {
when(response.getStatusLine()).thenReturn(mockStatusLine);
when(response.getEntity()).thenReturn(null);
- final Map<String, Object> decodedResponse = decoder.apply(response);
+ final TokenResponse decodedResponse = decoder.apply(response);
assertNull(decodedResponse);
}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoderTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoderTest.java
index 859d7cf..c4b0406 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoderTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoderTest.java
@@ -17,11 +17,11 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.decoding.impl;
+import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import java.io.ByteArrayInputStream;
@@ -37,10 +37,14 @@ import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.ErrorObject;
+import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
import net.shibboleth.idp.plugin.authn.oidc.rp.impl.AbstractOIDCTest;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse.UserInfoResponseType;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
/** Tests for the {@link DefaultUserInfoResponseDecoder}.*/
@@ -64,10 +68,20 @@ public class DefaultUserInfoResponseDecoderTest extends AbstractOIDCTest {
final StatusLine mockStatusLine = Mockito.mock(StatusLine.class);
when(mockStatusLine.getStatusCode()).thenReturn(500);
when(response.getStatusLine()).thenReturn(mockStatusLine);
+ when(response.getStatusLine()).thenReturn(mockStatusLine);
+
+ final Header[] headers = new Header[1];
+ headers[0] = new BasicHeader(
+ DefaultUserInfoResponseDecoder.USERINFO_ERROR_RESPONSE_HEADER,"Bearer realm=\"example\",\n"
+ + " error=\"invalid_token\",\n"
+ + " error_description=\"The access token expired\"");
+ when(response.getHeaders(DefaultUserInfoResponseDecoder.USERINFO_ERROR_RESPONSE_HEADER))
+ .thenReturn(headers);
final UserInfoResponse userInfoResponse = decoder.apply(response);
- assertNull(userInfoResponse);
-
+ assertFalse(userInfoResponse.indicatesSuccess());
+ final ErrorObject errorMsg = ((UserInfoErrorResponse)userInfoResponse).getErrorObject();
+ assertNotNull(errorMsg);
}
@Test
@@ -99,9 +113,10 @@ public class DefaultUserInfoResponseDecoderTest extends AbstractOIDCTest {
final UserInfoResponse userInfoResponse = decoder.apply(response);
assertNotNull(userInfoResponse);
- assertTrue(userInfoResponse.getType() == UserInfoResponseType.PLAIN);
- assertNotNull(userInfoResponse.getClaimsSet());
- assertEquals(userInfoResponse.getSub(), "248289761001");
+ assertTrue(userInfoResponse.indicatesSuccess());
+ assertNotNull(userInfoResponse.toSuccessResponse().getUserInfo());
+ assertNull(userInfoResponse.toSuccessResponse().getUserInfoJWT());
+ assertEquals(userInfoResponse.toSuccessResponse().getUserInfo().getSubject().getValue(), "248289761001");
}
@@ -124,7 +139,6 @@ public class DefaultUserInfoResponseDecoderTest extends AbstractOIDCTest {
when(mockHttpEntity.getContent()).thenReturn(new ByteArrayInputStream(USERINFO_RESPONSE_JWS.getBytes()));
final UserInfoResponse userInfoResponse = decoder.apply(response);
-
assertNull(userInfoResponse);
}
@@ -139,11 +153,28 @@ public class DefaultUserInfoResponseDecoderTest extends AbstractOIDCTest {
when(response.getEntity()).thenReturn(mockHttpEntity);
// Set the WRONG content type here
when(mockHttpEntity.getContentType()).thenReturn(
- new BasicHeader("Content-Type",DefaultUserInfoResponseDecoder.APPLICATION_JWT.toString()));
+ new BasicHeader("Content-Type",ContentType.APPLICATION_JWT.getType()));
when(mockHttpEntity.getContent()).thenReturn(new ByteArrayInputStream(USERINFO_RESPONSE.getBytes()));
final UserInfoResponse userInfoResponse = decoder.apply(response);
+ assertNull(userInfoResponse);
+ }
+
+ @Test
+ public void testEncoder_UnknownContentType() throws Exception {
+ decoder.initialize();
+ final HttpResponse response = Mockito.mock(HttpResponse.class);
+ final HttpEntity mockHttpEntity = Mockito.mock(HttpEntity.class);
+ final StatusLine mockStatusLine = Mockito.mock(StatusLine.class);
+ when(mockStatusLine.getStatusCode()).thenReturn(200);
+ when(response.getStatusLine()).thenReturn(mockStatusLine);
+ when(response.getEntity()).thenReturn(mockHttpEntity);
+ // Set the WRONG content type here
+ when(mockHttpEntity.getContentType()).thenReturn(
+ new BasicHeader("Content-Type","application/unknown"));
+ when(mockHttpEntity.getContent()).thenReturn(new ByteArrayInputStream(USERINFO_RESPONSE_JWS.getBytes()));
+ final UserInfoResponse userInfoResponse = decoder.apply(response);
assertNull(userInfoResponse);
}
@@ -157,16 +188,19 @@ public class DefaultUserInfoResponseDecoderTest extends AbstractOIDCTest {
when(response.getStatusLine()).thenReturn(mockStatusLine);
when(response.getEntity()).thenReturn(mockHttpEntity);
when(mockHttpEntity.getContentType()).thenReturn(
- new BasicHeader("Content-Type",DefaultUserInfoResponseDecoder.APPLICATION_JWT.toString()));
+ new BasicHeader("Content-Type",ContentType.APPLICATION_JWT.getType()));
when(mockHttpEntity.getContent()).thenReturn(new ByteArrayInputStream(USERINFO_RESPONSE_JWS.getBytes()));
- final UserInfoResponse userInfoResponse = decoder.apply(response);
+ final UserInfoResponse userInfoResponse = decoder.apply(response);
assertNotNull(userInfoResponse);
- assertTrue(userInfoResponse.getType() == UserInfoResponseType.JWT);
- assertTrue(userInfoResponse.isSigned());
- assertNotNull(userInfoResponse.getClaimsSet());
- assertEquals(userInfoResponse.getSub(), "user-subject-1234531");
- assertTrue(userInfoResponse.isClaimsSetAvailable());
+ assertTrue(userInfoResponse.indicatesSuccess());
+ assertEquals(userInfoResponse.toSuccessResponse().getEntityContentType(), ContentType.APPLICATION_JWT);
+ assertNull(userInfoResponse.toSuccessResponse().getUserInfo());
+ assertNotNull(userInfoResponse.toSuccessResponse().getUserInfoJWT());
+ assertTrue(userInfoResponse.toSuccessResponse().getUserInfoJWT() instanceof SignedJWT);
+ assertNotNull(userInfoResponse.toSuccessResponse().getUserInfoJWT().getJWTClaimsSet());
+ assertEquals(userInfoResponse.toSuccessResponse().getUserInfoJWT().getJWTClaimsSet()
+ .getSubject(), "user-subject-1234531");
}
@Test
@@ -179,14 +213,17 @@ public class DefaultUserInfoResponseDecoderTest extends AbstractOIDCTest {
when(response.getStatusLine()).thenReturn(mockStatusLine);
when(response.getEntity()).thenReturn(mockHttpEntity);
final Header[] headers = new Header[1];
- headers[0] = new BasicHeader("WWW-Authenticate"," Bearer realm=\"example\",\n"
- + "error=\"invalid_token\",\n"
- + "error_description=\"The access token expired\"");
- when(response.getHeaders("WWW-Authenticate")).thenReturn(headers);
+ headers[0] = new BasicHeader(
+ DefaultUserInfoResponseDecoder.USERINFO_ERROR_RESPONSE_HEADER,"Bearer realm=\"example\",\n"
+ + " error=\"invalid_token\",\n"
+ + " error_description=\"The access token expired\"");
+ when(response.getHeaders(DefaultUserInfoResponseDecoder.USERINFO_ERROR_RESPONSE_HEADER))
+ .thenReturn(headers);
final UserInfoResponse userInfoResponse = decoder.apply(response);
- assertNull(userInfoResponse);
+ assertNotNull(userInfoResponse);
+ assertFalse(userInfoResponse.indicatesSuccess());
}
@Test
@@ -199,14 +236,17 @@ public class DefaultUserInfoResponseDecoderTest extends AbstractOIDCTest {
when(response.getStatusLine()).thenReturn(mockStatusLine);
when(response.getEntity()).thenReturn(mockHttpEntity);
when(mockHttpEntity.getContentType()).thenReturn(
- new BasicHeader("Content-Type",DefaultUserInfoResponseDecoder.APPLICATION_JWT.toString()));
+ new BasicHeader("Content-Type",ContentType.APPLICATION_JWT.getType()));
when(mockHttpEntity.getContent()).thenReturn(new ByteArrayInputStream(USERINFO_RESPONSE_JWE.getBytes()));
- final UserInfoResponse userInfoResponse = decoder.apply(response);
+ final UserInfoResponse userInfoResponse = decoder.apply(response);
assertNotNull(userInfoResponse);
- assertTrue(userInfoResponse.getType() == UserInfoResponseType.JWT);
- assertTrue(userInfoResponse.isEncrypted());
- assertFalse(userInfoResponse.isClaimsSetAvailable());
+ assertTrue(userInfoResponse.indicatesSuccess());
+ assertEquals(userInfoResponse.toSuccessResponse().getEntityContentType(), ContentType.APPLICATION_JWT);
+ assertNull(userInfoResponse.toSuccessResponse().getUserInfo());
+ assertNotNull(userInfoResponse.toSuccessResponse().getUserInfoJWT());
+ assertTrue(userInfoResponse.toSuccessResponse().getUserInfoJWT() instanceof EncryptedJWT);
+
}
}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java
index e8ce877..42de561 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java
@@ -19,14 +19,18 @@ package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
import java.net.URI;
import java.time.Duration;
+import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
+import org.apache.http.HttpResponse;
import org.opensaml.messaging.context.MessageContext;
import org.opensaml.profile.context.ProfileRequestContext;
import org.springframework.webflow.execution.RequestContext;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.id.State;
import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser;
@@ -102,7 +106,9 @@ public abstract class AbstractOIDCTest {
@Nonnull @NotEmpty
protected final String ACCESS_TOKEN_RESPONSE = "{\n"
+ " \"access_token\": \"W0y5aDNAzEPNpSzu1cuMG904BZuQFZJUUwG5F3ct0zydZWy1ji\",\n"
+ + "\"refresh_token\": \"W0y5aDNAzEPNpSzu1cuMG904BZuQFZJUUwG5F3ct0zydZWy1ji\",\n"
+ " \"token_type\": \"Bearer\",\n"
+ + " \"expires_in\": 3600,\n"
+ " \"id_token\": \"eyJraWQiOiJiNjliYzcyOS05NDJjLTQzNjItYmM2YS03OWU3MjAwOWY0Yzg"
+ "iLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiM0pBZmFibUx4eWVnNjJJM2JiT0RrdyIsInN1YiI"
+ "6InVzZXItc3ViamVjdC0xMjM0NTMxIiwiYXVkIjoibXl0ZXN0Y2xpZW50IiwiaXNzIjoiaHR0cHM6"
@@ -114,6 +120,39 @@ public abstract class AbstractOIDCTest {
+ "o8rji7juk3szTOclbsYFrrd9iokWHl0qJa0G1ev5XQLsiUxYxgO1Nw-CAnHtESZ9w7JIfLLX1YlY73g\",\n"
+ " \"scope\": \"openid\"\n"
+ "}";
+
+
+ @Nonnull @NotEmpty
+ protected final String ACCESS_TOKEN_RESPONSE_NO_ACCESSTOKEN = "{\n"
+ + " \"refresh_token\": \"W0y5aDNAzEPNpSzu1cuMG904BZuQFZJUUwG5F3ct0zydZWy1ji\",\n"
+ + " \"token_type\": \"Bearer\",\n"
+ + " \"expires_in\": 3600,\n"
+ + " \"id_token\": \"eyJraWQiOiJiNjliYzcyOS05NDJjLTQzNjItYmM2YS03OWU3MjAwOWY0Yzg"
+ + "iLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiM0pBZmFibUx4eWVnNjJJM2JiT0RrdyIsInN1YiI"
+ + "6InVzZXItc3ViamVjdC0xMjM0NTMxIiwiYXVkIjoibXl0ZXN0Y2xpZW50IiwiaXNzIjoiaHR0cHM6"
+ + "XC9cL3d3dy5jZXJ0aWZpY2F0aW9uLm9wZW5pZC5uZXRcL3Rlc3RcL2FcL3Rlc3RfcnBfcHJveHlcLy"
+ + "IsImV4cCI6MTY0MzI3NTQyNCwiaWF0IjoxNjQzMjc1MTI0fQ.igU798iSZYUR7Z6_MQQendFCqm9Pf"
+ + "6Xpd4KtnHcPGG5FxtXPiDhlyhP_7ewSIQtzWxQ5wEatixDJWl2oRasivNCJyl6ZNeUG4YuT8S7xlgp"
+ + "UumoycBxrmomS7wgZSola8oZo8BuiRKlWzqLLtFkAChGi5iJEtHfVBe3k3PyQAsWRQdtixkvdxvyLq"
+ + "FLznIaPmMH-OZ82w7sJf4bpalZSAaR5NLpyzLFw-r81IOtOVrpQtnycCw0w_JxAP16GyZXCJc56vCE"
+ + "o8rji7juk3szTOclbsYFrrd9iokWHl0qJa0G1ev5XQLsiUxYxgO1Nw-CAnHtESZ9w7JIfLLX1YlY73g\",\n"
+ + " \"scope\": \"openid\"\n"
+ + "}";
+
+ @Nonnull @NotEmpty
+ protected final String ACCESS_TOKEN_RESPONSE_NO_IDTOKEN = "{\n"
+ + " \"access_token\": \"W0y5aDNAzEPNpSzu1cuMG904BZuQFZJUUwG5F3ct0zydZWy1ji\",\n"
+ + " \"refresh_token\": \"W0y5aDNAzEPNpSzu1cuMG904BZuQFZJUUwG5F3ct0zydZWy1ji\",\n"
+ + " \"token_type\": \"Bearer\",\n"
+ + " \"expires_in\": 3600,\n"
+ + " \"scope\": \"openid\"\n"
+ + "}";
+
+ @Nonnull @NotEmpty
+ protected final String TOKEN_RESPONSE_ERROR = "{\n"
+ + " \"error\":\"invalid_request\",\n"
+ + " \"error_description\":\"this request was bad\"\n"
+ + " }";
/** Mock a UserInfo JWE reponse.*/
@Nonnull @NotEmpty
@@ -271,5 +310,14 @@ public abstract class AbstractOIDCTest {
prc.setInboundMessageContext(inMsgCtx);
prc.setOutboundMessageContext(outMsgCtx);
}
+
+
+ protected JSONObject convertHttpResponseToJSONObject(final HttpResponse response) throws Exception {
+ final ObjectMapper mapper = new ObjectMapper();
+ final Map<String, Object> tokenResponseAsMap = mapper.readValue(
+ response.getEntity().getContent(), new TypeReference<Map<String, Object>>() {});
+ return new JSONObject(tokenResponseAsMap);
+ }
+
}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForTokenTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForAccessTokenTest.java
similarity index 56%
rename from idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForTokenTest.java
rename to idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForAccessTokenTest.java
index b4750c6..9db95bc 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForTokenTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForAccessTokenTest.java
@@ -21,11 +21,10 @@ package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
+import static org.testng.Assert.fail;
-import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
-import java.util.Map;
import javax.annotation.Nonnull;
@@ -42,13 +41,16 @@ import org.mockito.Mockito;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.messaging.context.navigate.ParentContextLookup;
import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.http.MediaType;
import org.springframework.webflow.execution.Event;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
-import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.util.StandardCharset;
+import com.nimbusds.oauth2.sdk.TokenErrorResponse;
+import com.nimbusds.oauth2.sdk.TokenResponse;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
@@ -56,7 +58,7 @@ import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileR
/**
* Tests for the {@link ExchangeCodeForAccessToken} action.
*/
-public class ExchangeCodeForTokenTest extends AbstractOIDCTest {
+public class ExchangeCodeForAccessTokenTest extends AbstractOIDCTest {
/** The action to test.*/
@Nonnull private ExchangeCodeForAccessToken exchangeAction;
@@ -92,7 +94,8 @@ public class ExchangeCodeForTokenTest extends AbstractOIDCTest {
Mockito.when(httpResponse.getStatusLine()).thenReturn(statusLine);
Mockito.when(statusLine.getStatusCode()).thenReturn(200);
- Mockito.when(httpResponse.getEntity()).thenReturn(new StringEntity(ACCESS_TOKEN_RESPONSE));
+ Mockito.when(httpResponse.getEntity()).thenReturn(
+ new StringEntity(ACCESS_TOKEN_RESPONSE, ContentType.parse(MediaType.APPLICATION_JSON_VALUE)));
Mockito.when(httpClient.execute((HttpUriRequest) Mockito.any(), (HttpContext) Mockito.any()))
.thenReturn(httpResponse);
@@ -119,9 +122,8 @@ public class ExchangeCodeForTokenTest extends AbstractOIDCTest {
exchangeAction.setHttpResponseDecoderStrategy(response -> {
final ObjectMapper mapper = new ObjectMapper();
try {
- return mapper.readValue(
- httpResponse.getEntity().getContent(), new TypeReference<Map<String, Object>>() {});
- } catch (final UnsupportedOperationException | IOException e) {
+ return OIDCTokenResponse.parse(convertHttpResponseToJSONObject(httpResponse));
+ } catch (final Exception e) {
return null;
}
});
@@ -134,6 +136,110 @@ public class ExchangeCodeForTokenTest extends AbstractOIDCTest {
}
+ @Test
+ public void testTokenExchange_WrongTokenType() throws Exception {
+
+ final HttpClient httpClient = Mockito.mock(HttpClient.class);
+ final HttpResponse httpResponse = Mockito.mock(HttpResponse.class);
+ final StatusLine statusLine = Mockito.mock(StatusLine.class);
+
+ Mockito.when(httpResponse.getStatusLine()).thenReturn(statusLine);
+
+ Mockito.when(statusLine.getStatusCode()).thenReturn(200);
+ Mockito.when(httpResponse.getEntity()).thenReturn(
+ new StringEntity(ACCESS_TOKEN_RESPONSE_NO_IDTOKEN, ContentType.parse(MediaType.APPLICATION_JSON_VALUE)));
+ Mockito.when(httpClient.execute((HttpUriRequest) Mockito.any(), (HttpContext) Mockito.any()))
+ .thenReturn(httpResponse);
+
+ // create new client with mock response
+ exchangeAction.setHttpClient(httpClient);
+
+ exchangeAction.setHttpRequestEncoderStrategy(prc -> {
+ URI uri;
+ try {
+ uri = new URIBuilder().setScheme("https")
+ .setHost("op.example.com")
+ .setPath("/token")
+ .build();
+ } catch (final URISyntaxException e) {
+ return null;
+ }
+
+ // Add headers and create request.
+ final RequestBuilder rb = RequestBuilder.post().setUri(uri)
+ .setHeader("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType())
+ .setCharset(StandardCharset.UTF_8);
+ return rb.build();
+ });
+ exchangeAction.setHttpResponseDecoderStrategy(response -> {
+ final ObjectMapper mapper = new ObjectMapper();
+ try {
+ return TokenResponse.parse(convertHttpResponseToJSONObject(httpResponse));
+ } catch (final Exception e) {
+ return null;
+ }
+ });
+
+ exchangeAction.initialize();
+
+ final Event event = exchangeAction.execute(src);
+ assertNotNull(event);
+ assertEquals("AuthenticationException",event.getId());
+
+ }
+
+ @Test
+ public void testTokenExchange_ErrorResponse() throws Exception {
+
+ final HttpClient httpClient = Mockito.mock(HttpClient.class);
+ final HttpResponse httpResponse = Mockito.mock(HttpResponse.class);
+ final StatusLine statusLine = Mockito.mock(StatusLine.class);
+
+ Mockito.when(httpResponse.getStatusLine()).thenReturn(statusLine);
+
+ Mockito.when(statusLine.getStatusCode()).thenReturn(400);
+ Mockito.when(httpResponse.getEntity()).thenReturn(
+ new StringEntity(TOKEN_RESPONSE_ERROR, ContentType.parse(MediaType.APPLICATION_JSON_VALUE)));
+ Mockito.when(httpClient.execute((HttpUriRequest) Mockito.any(), (HttpContext) Mockito.any()))
+ .thenReturn(httpResponse);
+
+ // create new client with mock response
+ exchangeAction.setHttpClient(httpClient);
+
+ exchangeAction.setHttpRequestEncoderStrategy(prc -> {
+ URI uri;
+ try {
+ uri = new URIBuilder().setScheme("https")
+ .setHost("op.example.com")
+ .setPath("/token")
+ .build();
+ } catch (final URISyntaxException e) {
+ return null;
+ }
+
+ // Add headers and create request.
+ final RequestBuilder rb = RequestBuilder.post().setUri(uri)
+ .setHeader("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType())
+ .setCharset(StandardCharset.UTF_8);
+ return rb.build();
+ });
+ exchangeAction.setHttpResponseDecoderStrategy(response -> {
+ try {
+ return TokenErrorResponse.parse(convertHttpResponseToJSONObject(httpResponse));
+ } catch (final Exception e) {
+ fail(e.getMessage());
+ return null;
+ }
+ });
+
+ exchangeAction.initialize();
+
+ final Event event = exchangeAction.execute(src);
+ assertNotNull(event);
+ assertEquals("AuthenticationException",event.getId());
+
+ }
+
@Test
public void testNullTokenResponse() throws Exception {
@@ -168,7 +274,6 @@ public class ExchangeCodeForTokenTest extends AbstractOIDCTest {
exchangeAction.initialize();
final Event event = exchangeAction.execute(src);
- //Null is success
assertNotNull(event);
assertEquals("AuthenticationException",event.getId());
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponseTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponseTest.java
deleted file mode 100644
index 5cd9818..0000000
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponseTest.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements. See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You 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.authn.oidc.rp.impl;
-
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertNull;
-
-import org.opensaml.messaging.context.navigate.ChildContextLookup;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.springframework.webflow.execution.Event;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
-import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
-import net.shibboleth.oidc.profile.core.OidcEventIds;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-/** Tests for ExtractIDTokenFromResponse.*/
-public class ExtractIDTokenFromResponseTest extends AbstractOIDCTest {
-
- private ExtractIDTokenFromResponse action;
-
-
- @Override
- @BeforeMethod
- public void setup() throws Exception {
- super.setup();
- action = new ExtractIDTokenFromResponse();
-
- final AccessTokenResponseContext trc = new AccessTokenResponseContext();
- prc.getInboundMessageContext().addSubcontext(trc);
-
- action.setProfileContextLookupStrategy(new ChildContextLookup<>(ProfileRequestContext.class).compose(
- new ChildContextLookup<>(AuthenticationContext.class)
- .compose(new WebflowRequestContextProfileRequestContextLookup())));
- }
-
- @Test
- public void testExtractSignedToken_Success() throws ComponentInitializationException {
- action.setRawIdTokenLookupStrategy(prc -> "eyJraWQiOiJiNjliY"
- + "zcyOS05NDJjLTQzNjItYmM2YS03OWU3MjAwOWY0YzgiLCJhbG"
- + "ciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiM0pBZmFibUx4eWVnNj"
- + "JJM2JiT0RrdyIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NTM"
- + "xIiwiYXVkIjoibXl0ZXN0Y2xpZW50IiwiaXNzIjoiaHR0cHM6X"
- + "C9cL3d3dy5jZXJ0aWZpY2F0aW9uLm9wZW5pZC5uZXRcL3Rlc3"
- + "RcL2FcL3Rlc3RfcnBfcHJveHlcLyIsImV4cCI6MTY0MzI3NTQy"
- + "NCwiaWF0IjoxNjQzMjc1MTI0fQ.igU798iSZYUR7Z6_MQQend"
- + "FCqm9Pf6Xpd4KtnHcPGG5FxtXPiDhlyhP_7ewSIQtzWxQ5wEat"
- + "ixDJWl2oRasivNCJyl6ZNeUG4YuT8S7xlgpUumoycBxrmomS7"
- + "wgZSola8oZo8BuiRKlWzqLLtFkAChGi5iJEtHfVBe3k3PyQAs"
- + "WRQdtixkvdxvyLqFLznIaPmMH-OZ82w7sJf4bpalZSAaR5NLpy"
- + "zLFw-r81IOtOVrpQtnycCw0w_JxAP16GyZXCJc56vCEo8rji7"
- + "juk3szTOclbsYFrrd9iokWHl0qJa0G1ev5XQLsiUxYxgO1Nw-"
- + "CAnHtESZ9w7JIfLLX1YlY73g");
-
- action.initialize();
- final Event event = action.execute(src);
- assertNull(event);
- assertNotNull(prc.getInboundMessageContext().getSubcontext(AccessTokenResponseContext.class).getIdToken());
-
- }
-
- /* Header has been changed to use the 'none' algorithm', and signature removed*/
- @Test
- public void testExtractSignedToken_NoneAlgInjectedIntoHeaderSuccess() throws ComponentInitializationException {
- action.setRawIdTokenLookupStrategy(prc ->
- "eyJraWQiOiJiNjliYzcyOS05NDJjLTQzNjItYmM2YS03OWU3MjAwOWY0YzgiLCJhbGciOiJub25lIn0=."
- + "eyJhdF9oYXNoIjoiM0pBZmFibUx4eWVnNj"
- + "JJM2JiT0RrdyIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NTM"
- + "xIiwiYXVkIjoibXl0ZXN0Y2xpZW50IiwiaXNzIjoiaHR0cHM6X"
- + "C9cL3d3dy5jZXJ0aWZpY2F0aW9uLm9wZW5pZC5uZXRcL3Rlc3"
- + "RcL2FcL3Rlc3RfcnBfcHJveHlcLyIsImV4cCI6MTY0MzI3NTQy"
- + "NCwiaWF0IjoxNjQzMjc1MTI0fQ");
-
- action.initialize();
- final Event event = action.execute(src);
- assertNotNull(event);
- assertEquals(event.getId(), OidcEventIds.INVALID_ID_TOKEN);
-
- }
-
- //TODO need encrypted as well?
-
-}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/MergeUserInfoAndIDTokenClaimsTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/MergeUserInfoAndIDTokenClaimsTest.java
deleted file mode 100644
index b106ffc..0000000
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/MergeUserInfoAndIDTokenClaimsTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements. See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You 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.authn.oidc.rp.impl;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertNull;
-
-import java.time.Instant;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-
-import org.opensaml.messaging.context.navigate.ChildContextLookup;
-import org.opensaml.messaging.context.navigate.ParentContextLookup;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.springframework.webflow.execution.Event;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.jwt.PlainJWT;
-import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
-
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.EndUserClaimsContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.PlainUserInfoResponse;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse;
-import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
-import net.shibboleth.oidc.security.jwt.claims.impl.JWTClaims;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-/** Tests for {@link ProcessEndUserClaims}.*/
-public class MergeUserInfoAndIDTokenClaimsTest extends AbstractOIDCTest {
-
- /** Action to test.*/
- private ProcessEndUserClaims action;
-
-
- @BeforeMethod
- public void setup() throws Exception {
- super.setup();
- action = new ProcessEndUserClaims();
-
- final AccessTokenResponseContext trc = new AccessTokenResponseContext();
- final PlainJWT jwt = new PlainJWT(new JWTClaimsSet.Builder()
- .issuer("https://op.example.com")
- .audience(List.of("https://rp.example.com"))
- .subject("jdoe")
- .claim("nonce", "abadnonce")
- .claim("azp", "https://rp.example.com")
- .claim("name","jdoe")
- .expirationTime(Date.from(Instant.now().plusSeconds(120)))
- .build());
- trc.setIdToken(jwt);
- prc.getInboundMessageContext().addSubcontext(trc);
-
- final ClaimsSet claims = new ClaimsSet();
- claims.putAll(Map.of("sub","jdoe","given_name","John","email","jdoe at example.com"));
- final UserInfoResponse userInfoResponse = new PlainUserInfoResponse(claims);
- final UserInfoResponseContext urc = new UserInfoResponseContext();
- urc.setUserInfo(userInfoResponse);
- prc.getInboundMessageContext().addSubcontext(urc);
-
- action.setAuthenticationContextLookupStrategy(new ParentContextLookup<>(AuthenticationContext.class));
-
- action.setProfileContextLookupStrategy(new ChildContextLookup<>(ProfileRequestContext.class).compose(
- new ChildContextLookup<>(AuthenticationContext.class)
- .compose(new WebflowRequestContextProfileRequestContextLookup())));
- }
-
- @Test
- public void testSuccesfulMerge() throws ComponentInitializationException {
-
- action.initialize();
- final Event event = action.execute(src);
- assertNull(event);
- assertNotNull(prc.getInboundMessageContext().getSubcontext(EndUserClaimsContext.class).getEndUserClaims());
- final ClaimsSet claims =
- prc.getInboundMessageContext().getSubcontext(EndUserClaimsContext.class).getEndUserClaims();
- assertEquals(claims.getClaim("name"),"jdoe");
- assertEquals(claims.getClaim("sub"),"jdoe");
- assertEquals(claims.getClaim("given_name"),"John");
- assertEquals(claims.getClaim("email"),"jdoe at example.com");
- assertNull(claims.getIssuer());
- assertNull(claims.getAudience());
- assertNull(claims.getDateClaim(JWTClaims.EXPIRATION_TIME_CLAIM.getClaimName()));
- }
-
- @Test
- public void testSuccesfulMergeNoSanitization() throws ComponentInitializationException {
-
- action.setEnableClaimSanitizationStrategy(false);
- action.initialize();
- final Event event = action.execute(src);
- assertNull(event);
- assertNotNull(prc.getInboundMessageContext().getSubcontext(EndUserClaimsContext.class).getEndUserClaims());
- final ClaimsSet claims =
- prc.getInboundMessageContext().getSubcontext(EndUserClaimsContext.class).getEndUserClaims();
- assertEquals(claims.getClaim("name"),"jdoe");
- assertEquals(claims.getClaim("sub"),"jdoe");
- assertEquals(claims.getClaim("given_name"),"John");
- assertEquals(claims.getClaim("email"),"jdoe at example.com");
- assertNotNull(claims.getIssuer());
- assertNotNull(claims.getAudience());
- assertNotNull(claims.getDateClaim(JWTClaims.EXPIRATION_TIME_CLAIM.getClaimName()));
- }
-
-}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowFromAuthenticationResponseTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowFromAuthenticationResponseTest.java
index f00a9a9..c3d3e82 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowFromAuthenticationResponseTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowFromAuthenticationResponseTest.java
@@ -28,6 +28,7 @@ import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.authn.context.ExternalAuthenticationContext;
import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
import net.shibboleth.idp.authn.impl.ExternalAuthenticationImpl;
+import net.shibboleth.idp.plugin.authn.oidc.rp.test.TestTokenHelper;
import net.shibboleth.idp.saml.authn.principal.AuthenticationMethodPrincipal;
import net.shibboleth.oidc.authn.principal.AuthenticationContextClassReferencePrincipal;
import net.shibboleth.oidc.profile.config.JSONSecurityConfiguration;
@@ -688,5 +689,40 @@ public class OIDCRPFlowFromAuthenticationResponseTest extends OIDCRPFlowTest {
// Should have failed to produce a result
assertFlowEndedInErrorConditions(prc, "InvalidUserInfoClaims");
}
+
+ /**
+ * Test the flow from the external authorization request to the end of the flow.
+ * Using a MAC signed id_token and an invalid UserInfo JSON Response - it has no subject.
+ *
+ * @throws Exception on error.
+ */
+ @Test
+ public void test_IDTokenHS256_InvalidPlainUserInfo_SubDoesNotMatchIdToken() throws Exception {
+
+ basicSetup();
+
+ final MockWebServer mockOPServer = createSimpleServer();
+ final var accessTokenResp = TestTokenHelper.createAccessTokenResponseJSON(Map.of("iss", OP_ISSUER_ID, "azp",
+ CLIENT_ID, "aud", List.of(CLIENT_ID)), JWSAlgorithm.HS256, null, null,
+ new DefaultClientSecretCredential(CLIENT_SECRET).toSigningCredential(), null);
+ final var userInfoResp =
+ TestTokenHelper.createPlainUserInfoResponseString(
+ Map.of("sub","wrong_sub", "iss", OP_ISSUER_ID, "aud", List.of(CLIENT_ID)));
+
+ // First is token exchange
+ queueMockServerResponse(mockOPServer, 200, accessTokenResp, "application/json");
+ // Second is plain userInfo
+ queueMockServerResponse(mockOPServer, 200, userInfoResp, "application/json");
+
+ mockOPServer.start(9918);
+
+ final var prc = resumeBasicFlow();
+
+ mockOPServer.shutdown();
+
+ // Assert test conditions
+ // Should have failed to produce a result
+ assertFlowEndedInErrorConditions(prc, "InvalidUserInfoClaims");
+ }
}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
index 586adbc..2540765 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
@@ -48,6 +48,7 @@ import org.springframework.webflow.engine.Flow;
import org.springframework.webflow.engine.impl.FlowExecutionImpl;
import org.springframework.webflow.test.MockFlowBuilderContext;
+import com.nimbusds.common.contenttype.ContentType;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.ParseException;
@@ -72,8 +73,6 @@ import net.shibboleth.idp.plugin.authn.oidc.rp.context.EndUserClaimsContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.OAuth2ClientContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.OIDCPeerEntityContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.JWTUserInfoResponse;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.PlainUserInfoResponse;
import net.shibboleth.idp.plugin.authn.oidc.rp.principal.OIDCSubjectIdentifierPrincipal;
import net.shibboleth.idp.plugin.authn.test.flow.AbstractAuthnXmlFlowExecutionTests;
import net.shibboleth.idp.plugin.authn.test.flow.mock.MockFlowBuilder;
@@ -676,9 +675,10 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
throws java.text.ParseException {
final var accessTokenResponse =
nestedPrc.getInboundMessageContext().getSubcontext(AccessTokenResponseContext.class);
- assertNotNull(accessTokenResponse.getIdToken());
- assertTrue(accessTokenResponse.getIdToken() instanceof SignedJWT);
- final var signedJwt = (SignedJWT) accessTokenResponse.getIdToken();
+ assertNotNull(accessTokenResponse.getTokenResponse().getOIDCTokens().getIDToken());
+ //TODO this might change if the token we mutate is not in the original token response
+ assertTrue(accessTokenResponse.getTokenResponse().getOIDCTokens().getIDToken() instanceof SignedJWT);
+ final var signedJwt = (SignedJWT) accessTokenResponse.getTokenResponse().getOIDCTokens().getIDToken();
assertEquals(signedJwt.getState(), com.nimbusds.jose.JWSObject.State.VERIFIED);
assertStandardIdTokenClaimsSuccessCondition(signedJwt.getJWTClaimsSet());
}
@@ -695,9 +695,10 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
final var userInfoResponse =
nestedPrc.getInboundMessageContext().getSubcontext(UserInfoResponseContext.class);
assertNotNull(userInfoResponse.getUserInfo());
- assertTrue(userInfoResponse.getUserInfo() instanceof JWTUserInfoResponse);
- final var jwtResponse = (JWTUserInfoResponse) userInfoResponse.getUserInfo();
- final var signedJwt = (SignedJWT) jwtResponse.getResponseJwt();
+ assertEquals(userInfoResponse.getUserInfo().getEntityContentType(), ContentType.APPLICATION_JWT);
+ final var jwtResponse = userInfoResponse.getUserInfo().getUserInfoJWT();
+ assertTrue(jwtResponse instanceof SignedJWT);
+ final var signedJwt = (SignedJWT) jwtResponse;
assertEquals(signedJwt.getState(), com.nimbusds.jose.JWSObject.State.VERIFIED);
}
@@ -714,7 +715,7 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
final var userInfoResponse =
nestedPrc.getInboundMessageContext().getSubcontext(UserInfoResponseContext.class);
assertNotNull(userInfoResponse.getUserInfo());
- assertTrue(userInfoResponse.getUserInfo() instanceof PlainUserInfoResponse);
+ assertTrue(userInfoResponse.getUserInfo().getEntityContentType() == ContentType.APPLICATION_JSON);
}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ProcessEndUserClaimsTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ProcessEndUserClaimsTest.java
index fde38ec..886d33b 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ProcessEndUserClaimsTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ProcessEndUserClaimsTest.java
@@ -23,10 +23,13 @@ import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.fail;
+import java.time.Instant;
+import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
+import org.mockito.Mockito;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.messaging.context.navigate.ParentContextLookup;
import org.opensaml.profile.action.EventIds;
@@ -38,18 +41,24 @@ import org.testng.annotations.Test;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.PlainJWT;
import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.token.RefreshToken;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.UserInfo;
+import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.EndUserClaimsContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.PlainUserInfoResponse;
import net.shibboleth.idp.profile.context.RelyingPartyContext;
import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
import net.shibboleth.idp.relyingparty.RelyingPartyConfiguration;
import net.shibboleth.oidc.profile.config.OIDCAuthorizationConfiguration;
import net.shibboleth.oidc.security.jwt.claims.impl.JWTClaims;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
/** Tests for {@link ProcessEndUserClaims}.*/
public class ProcessEndUserClaimsTest extends AbstractOIDCTest {
@@ -94,7 +103,8 @@ public class ProcessEndUserClaimsTest extends AbstractOIDCTest {
action.setClaimSanitizationStrategy(claims -> {
final Set<String> validationClaims = Set.of(
JWTClaims.ISSUER_CLAIM.getClaimName(),
- JWTClaims.AUDIENCE_CLAIM.getClaimName());
+ JWTClaims.AUDIENCE_CLAIM.getClaimName(),
+ JWTClaims.EXPIRATION_TIME_CLAIM.getClaimName());
final ClaimsSet sanitizedClaims = new ClaimsSet();
final Map<String, Object> filteredMap = claims.toJSONObject().entrySet()
@@ -107,13 +117,20 @@ public class ProcessEndUserClaimsTest extends AbstractOIDCTest {
action.setAccessTokenResponseContextLookupStrategy(prc -> {
final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
+ final OIDCTokenResponse tokenResponse = Mockito.mock(OIDCTokenResponse.class);
+ atrc.setTokenResponse(tokenResponse);
final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.issuer("https://op.example.com")
.audience("https://rp.example.com")
- .subject("joe")
+ .subject("jdoe")
.claim("family_name", "blogs")
+ .claim("name","jdoe")
+ .expirationTime(Date.from(Instant.now().plusSeconds(120)))
.build();
- atrc.setIdToken(new PlainJWT(claimsSet));
+ final OIDCTokens tokens = new OIDCTokens(new PlainJWT(claimsSet),
+ new BearerAccessToken("access_token_value"),
+ new RefreshToken("refresh_token_value"));
+ Mockito.when(tokenResponse.getOIDCTokens()).thenReturn(tokens);
return atrc;
});
@@ -122,7 +139,8 @@ public class ProcessEndUserClaimsTest extends AbstractOIDCTest {
final ClaimsSet claims = new ClaimsSet();
claims.setClaim("given_name", "joe");
claims.setClaim("email", "joe at example.com");
- final PlainUserInfoResponse uir = new PlainUserInfoResponse(claims);
+ claims.setClaim("sub", "jdoe");
+ final UserInfoSuccessResponse uir = new UserInfoSuccessResponse(new UserInfo(claims.toJSONObject()));
uirc.setUserInfo(uir);
return uirc;
});
@@ -138,12 +156,49 @@ public class ProcessEndUserClaimsTest extends AbstractOIDCTest {
prc.getInboundMessageContext().getSubcontext(EndUserClaimsContext.class);
assertNull(claimsContext.getEndUserClaims().getClaim("iss"));
assertNull(claimsContext.getEndUserClaims().getClaim("aud"));
- assertEquals(claimsContext.getEndUserClaims().getClaim("sub"), "joe");
+ assertEquals(claimsContext.getEndUserClaims().getClaim("sub"), "jdoe");
assertEquals(claimsContext.getEndUserClaims().getClaim("family_name"), "blogs");
assertEquals(claimsContext.getEndUserClaims().getClaim("given_name"), "joe");
assertEquals(claimsContext.getEndUserClaims().getClaim("email"), "joe at example.com");
}
+ @Test
+ public void testSuccesfulMerge_ExpirySanatized() throws ComponentInitializationException {
+
+ action.initialize();
+ final Event event = action.execute(src);
+ assertNull(event);
+ assertNotNull(prc.getInboundMessageContext().getSubcontext(EndUserClaimsContext.class).getEndUserClaims());
+ final ClaimsSet claims =
+ prc.getInboundMessageContext().getSubcontext(EndUserClaimsContext.class).getEndUserClaims();
+ assertEquals(claims.getClaim("name"),"jdoe");
+ assertEquals(claims.getClaim("sub"),"jdoe");
+ assertEquals(claims.getClaim("given_name"),"joe");
+ assertEquals(claims.getClaim("email"),"joe at example.com");
+ assertNull(claims.getIssuer());
+ assertNull(claims.getAudience());
+ assertNull(claims.getDateClaim(JWTClaims.EXPIRATION_TIME_CLAIM.getClaimName()));
+ }
+
+ @Test
+ public void testSuccesfulMerge_NoSanitization() throws ComponentInitializationException {
+
+ action.setEnableClaimSanitizationStrategy(false);
+ action.initialize();
+ final Event event = action.execute(src);
+ assertNull(event);
+ assertNotNull(prc.getInboundMessageContext().getSubcontext(EndUserClaimsContext.class).getEndUserClaims());
+ final ClaimsSet claims =
+ prc.getInboundMessageContext().getSubcontext(EndUserClaimsContext.class).getEndUserClaims();
+ assertEquals(claims.getClaim("name"),"jdoe");
+ assertEquals(claims.getClaim("sub"),"jdoe");
+ assertEquals(claims.getClaim("given_name"),"joe");
+ assertEquals(claims.getClaim("email"),"joe at example.com");
+ assertNotNull(claims.getIssuer());
+ assertNotNull(claims.getAudience());
+ assertNotNull(claims.getDateClaim(JWTClaims.EXPIRATION_TIME_CLAIM.getClaimName()));
+ }
+
@Test
public void testSuccess_NoUserInfoClaims() throws Exception {
@@ -157,18 +212,20 @@ public class ProcessEndUserClaimsTest extends AbstractOIDCTest {
prc.getInboundMessageContext().getSubcontext(EndUserClaimsContext.class);
assertNull(claimsContext.getEndUserClaims().getClaim("iss"));
assertNull(claimsContext.getEndUserClaims().getClaim("aud"));
- assertEquals(claimsContext.getEndUserClaims().getClaim("sub"), "joe");
+ assertEquals(claimsContext.getEndUserClaims().getClaim("sub"), "jdoe");
assertEquals(claimsContext.getEndUserClaims().getClaim("family_name"), "blogs");
assertNull(claimsContext.getEndUserClaims().getClaim("given_name"), "joe");
assertNull(claimsContext.getEndUserClaims().getClaim("email"), "joe at example.com");
}
@Test
- public void testSuccess_NoIDToken() throws Exception {
+ public void testSuccess_NoOIDCToken() throws Exception {
action.setAccessTokenResponseContextLookupStrategy(prc -> {
final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
- atrc.setIdToken(null);
+ final OIDCTokenResponse tokenResponse = Mockito.mock(OIDCTokenResponse.class);
+ Mockito.when(tokenResponse.getOIDCTokens()).thenReturn(null);
+ atrc.setTokenResponse(tokenResponse);
return atrc;
});
@@ -184,10 +241,15 @@ public class ProcessEndUserClaimsTest extends AbstractOIDCTest {
action.setAccessTokenResponseContextLookupStrategy(prc -> {
final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
+ final OIDCTokenResponse tokenResponse = Mockito.mock(OIDCTokenResponse.class);
+ atrc.setTokenResponse(tokenResponse);
try {
- atrc.setIdToken(new PlainJWT(new ClaimsSet().toJWTClaimsSet()));
+ final OIDCTokens tokens = new OIDCTokens(new PlainJWT(new ClaimsSet().toJWTClaimsSet()),
+ new BearerAccessToken("access_token_value"),
+ new RefreshToken("refresh_token_value"));
+ Mockito.when(tokenResponse.getOIDCTokens()).thenReturn(tokens);
} catch (final ParseException e) {
- fail(e.getMessage());
+ fail(e.getMessage());
}
return atrc;
});
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TokenResponseIDTokenLookupStrategyTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TokenResponseIDTokenLookupStrategyTest.java
deleted file mode 100644
index 639a6c0..0000000
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TokenResponseIDTokenLookupStrategyTest.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertNull;
-
-import java.io.ByteArrayInputStream;
-import java.util.Map;
-
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.navigate.TokenResponseIDTokenLookupStrategy;
-
-/** Tests for TokenResponseIDTokenLookupStrategy.*/
-public class TokenResponseIDTokenLookupStrategyTest extends AbstractOIDCTest {
-
- /** The strategy to test.*/
- private TokenResponseIDTokenLookupStrategy strategy;
-
-
- @BeforeMethod
- public void setup() throws Exception {
- super.setup();
- strategy = new TokenResponseIDTokenLookupStrategy();
- ObjectMapper mapper = new ObjectMapper();
-
- final Map<String, Object> rawResponse = mapper.readValue(
- new ByteArrayInputStream(ACCESS_TOKEN_RESPONSE.getBytes()), new TypeReference<Map<String, Object>>() {});
- final AccessTokenResponseContext trc = new AccessTokenResponseContext();
- trc.setRawTokenResponse(rawResponse);
- prc.getInboundMessageContext().addSubcontext(trc);
-
- }
-
- @Test
- public void testLookup_Success() {
- final String rawToken = strategy.apply(prc);
- assertNotNull(rawToken);
- assertEquals(rawToken, "eyJraWQiOiJiNjliYzcyOS05NDJjLTQzNjItYmM2YS0"
- + "3OWU3MjAwOWY0YzgiLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiM"
- + "0pBZmFibUx4eWVnNjJJM2JiT0RrdyIsInN1YiI6InVzZXItc3ViamVj"
- + "dC0xMjM0NTMxIiwiYXVkIjoibXl0ZXN0Y2xpZW50IiwiaXNzIjoiaHR0"
- + "cHM6XC9cL3d3dy5jZXJ0aWZpY2F0aW9uLm9wZW5pZC5uZXRcL3Rlc3Rc"
- + "L2FcL3Rlc3RfcnBfcHJveHlcLyIsImV4cCI6MTY0MzI3NTQyNCwiaWF0"
- + "IjoxNjQzMjc1MTI0fQ.igU798iSZYUR7Z6_MQQendFCqm9Pf6Xpd4Ktn"
- + "HcPGG5FxtXPiDhlyhP_7ewSIQtzWxQ5wEatixDJWl2oRasivNCJyl6ZN"
- + "eUG4YuT8S7xlgpUumoycBxrmomS7wgZSola8oZo8BuiRKlWzqLLtFkAC"
- + "hGi5iJEtHfVBe3k3PyQAsWRQdtixkvdxvyLqFLznIaPmMH-OZ82w7sJf"
- + "4bpalZSAaR5NLpyzLFw-r81IOtOVrpQtnycCw0w_JxAP16GyZXCJc56v"
- + "CEo8rji7juk3szTOclbsYFrrd9iokWHl0qJa0G1ev5XQLsiUxYxgO1Nw"
- + "-CAnHtESZ9w7JIfLLX1YlY73g");
- }
-
- @Test
- public void testLookup_NoResponseContext() {
- prc.getInboundMessageContext().removeSubcontext(AccessTokenResponseContext.class);
- final String rawToken = strategy.apply(prc);
- assertNull(rawToken);
-
- }
-
- @Test
- public void testLookup_NoInboundContext() {
- prc.setInboundMessageContext(null);
- final String rawToken = strategy.apply(prc);
- assertNull(rawToken);
-
- }
-
-}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/UserInfoEndpointLookupTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/UserInfoEndpointLookupTest.java
index 45c7694..ce81ceb 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/UserInfoEndpointLookupTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/UserInfoEndpointLookupTest.java
@@ -21,11 +21,10 @@ package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
+import static org.testng.Assert.fail;
-import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
-import java.util.Map;
import javax.annotation.Nonnull;
@@ -35,7 +34,6 @@ import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.utils.URIBuilder;
-import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.protocol.HttpContext;
import org.mockito.Mockito;
@@ -46,16 +44,16 @@ import org.springframework.webflow.execution.Event;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.common.contenttype.ContentType;
import com.nimbusds.jose.util.StandardCharset;
-import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.UserInfo;
-import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.PlainUserInfoResponse;
import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
/**
* Tests for the {@link UserInfoEndpointLookup} action.
@@ -116,19 +114,15 @@ public class UserInfoEndpointLookupTest extends AbstractOIDCTest {
// Add headers and create request.
final RequestBuilder rb = RequestBuilder.post().setUri(uri)
- .setHeader("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType())
+ .setHeader("Content-Type", ContentType.APPLICATION_URLENCODED.getType())
.setCharset(StandardCharset.UTF_8);
return rb.build();
});
action.setHttpResponseDecoderStrategy(response -> {
- final ObjectMapper mapper = new ObjectMapper();
try {
- final Map<String, Object> responseMap = mapper.readValue(
- httpResponse.getEntity().getContent(), new TypeReference<Map<String, Object>>() {});
- final ClaimsSet claims = new ClaimsSet();
- claims.putAll(responseMap);
- return new PlainUserInfoResponse(claims);
- } catch (final UnsupportedOperationException | IOException e) {
+ return new UserInfoSuccessResponse(new UserInfo(convertHttpResponseToJSONObject(httpResponse)));
+ } catch (final Exception e) {
+ fail(e.getMessage());
return null;
}
});
@@ -141,10 +135,63 @@ public class UserInfoEndpointLookupTest extends AbstractOIDCTest {
assertNotNull(prc.getInboundMessageContext().getSubcontext(UserInfoResponseContext.class));
assertNotNull(prc.getInboundMessageContext().getSubcontext(UserInfoResponseContext.class)
.getUserInfo());
+ assertEquals(prc.getInboundMessageContext().getSubcontext(UserInfoResponseContext.class)
+ .getUserInfo().getEntityContentType(), ContentType.APPLICATION_JSON);
assertNotNull(prc.getInboundMessageContext().getSubcontext(UserInfoResponseContext.class)
- .getUserInfo().getClaimsSet());
- assertNotNull(prc.getInboundMessageContext().getSubcontext(UserInfoResponseContext.class)
- .getUserInfo().getClaimsSet().getClaim("sub"),"248289761001");
+ .getUserInfo().getUserInfo().getClaim("sub"),"248289761001");
+
+ }
+
+ @Test
+ public void testUserInfoLookup_ErrorResponse() throws Exception {
+
+ final HttpClient httpClient = Mockito.mock(HttpClient.class);
+ final HttpResponse httpResponse = Mockito.mock(HttpResponse.class);
+ final StatusLine statusLine = Mockito.mock(StatusLine.class);
+
+ Mockito.when(httpResponse.getStatusLine()).thenReturn(statusLine);
+
+ Mockito.when(statusLine.getStatusCode()).thenReturn(401);
+ Mockito.when(httpResponse.getEntity()).thenReturn(new StringEntity(TOKEN_RESPONSE_ERROR));
+ Mockito.when(httpClient.execute((HttpUriRequest) Mockito.any(), (HttpContext) Mockito.any()))
+ .thenReturn(httpResponse);
+
+ // create new client with mock response
+ action.setHttpClient(httpClient);
+
+ action.setHttpRequestEncoderStrategy(prc -> {
+ URI uri;
+ try {
+ uri = new URIBuilder().setScheme("https")
+ .setHost("op.example.com")
+ .setPath("/userinfo")
+ .build();
+ } catch (final URISyntaxException e) {
+ return null;
+ }
+
+ // Add headers and create request.
+ final RequestBuilder rb = RequestBuilder.post().setUri(uri)
+ .setHeader("Content-Type", ContentType.APPLICATION_URLENCODED.getType())
+ .setCharset(StandardCharset.UTF_8);
+ return rb.build();
+ });
+ action.setHttpResponseDecoderStrategy(response -> {
+ try {
+ return UserInfoErrorResponse.parse("Bearer realm=\"example.com\",\n"
+ + " error=\"invalid_token\",\n"
+ + " error_description=\"The access token expired\"");
+ } catch (final Exception e) {
+ fail(e.getMessage());
+ return null;
+ }
+ });
+
+ action.initialize();
+
+ final Event event = action.execute(src);
+ assertNotNull(event);
+ assertEquals(event.getId(), OidcEventIds.INVALID_USERINFO_CLAIMS);
}
@@ -168,12 +215,13 @@ public class UserInfoEndpointLookupTest extends AbstractOIDCTest {
.setPath("/userinfo")
.build();
} catch (final URISyntaxException e) {
+ fail(e.getMessage());
return null;
}
// Add headers and create request.
final RequestBuilder rb = RequestBuilder.post().setUri(uri)
- .setHeader("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType())
+ .setHeader("Content-Type", ContentType.APPLICATION_URLENCODED.getType())
.setCharset(StandardCharset.UTF_8);
return rb.build();
});
@@ -185,7 +233,7 @@ public class UserInfoEndpointLookupTest extends AbstractOIDCTest {
//Null is success
assertNotNull(event);
- assertEquals(event.getId(), AuthnEventIds.AUTHN_EXCEPTION);
+ assertEquals(event.getId(),OidcEventIds.INVALID_USERINFO_CLAIMS);
}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOAuthAccessTokenResponseTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOAuthAccessTokenResponseTest.java
index 0c57ed7..88b9f4c 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOAuthAccessTokenResponseTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOAuthAccessTokenResponseTest.java
@@ -1,17 +1,33 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
+import static org.junit.Assert.assertNotNull;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
+import java.time.Duration;
+import java.time.Instant;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.mockito.Mockito;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.messaging.context.navigate.ParentContextLookup;
import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.http.MediaType;
import org.springframework.webflow.execution.Event;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.token.DPoPAccessToken;
+import com.nimbusds.oauth2.sdk.token.RefreshToken;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
@@ -24,6 +40,16 @@ public class ValidateOAuthAccessTokenResponseTest extends AbstractOIDCTest {
/** The action to test.*/
private ValidateOAuthAccessTokenResponse action;
+ private HttpResponse httpResponse;
+
+ private AccessTokenResponseContext atrc;
+
+ private OIDCTokenResponse tokenResponse;
+
+ private OIDCTokens tokens;
+
+ private JWTClaimsSet claimsSet;
+
@Override
@BeforeMethod
@@ -36,27 +62,37 @@ public class ValidateOAuthAccessTokenResponseTest extends AbstractOIDCTest {
.compose(new WebflowRequestContextProfileRequestContextLookup())));
action.setAuthenticationContextLookupStrategy(new ParentContextLookup<>(AuthenticationContext.class));
+
+ httpResponse = Mockito.mock(HttpResponse.class);
+ final StatusLine statusLine = Mockito.mock(StatusLine.class);
+ Mockito.when(httpResponse.getStatusLine()).thenReturn(statusLine);
+ Mockito.when(statusLine.getStatusCode()).thenReturn(200);
+ Mockito.when(httpResponse.getEntity()).thenReturn(new StringEntity(ACCESS_TOKEN_RESPONSE,
+ ContentType.parse(MediaType.APPLICATION_JSON_VALUE)));
+
+ atrc = Mockito.mock(AccessTokenResponseContext.class);
+ tokenResponse = Mockito.mock(OIDCTokenResponse.class);
+ Mockito.when(atrc.getTokenResponse()).thenReturn(tokenResponse);
+
+ claimsSet = new JWTClaimsSet.Builder()
+ .issuer("https://op.example.com")
+ .audience("https://rp.example.com")
+ .subject("joe")
+ .claim("family_name", "blogs")
+ .build();
+
+ tokens = new OIDCTokens(new PlainJWT(claimsSet),
+ new BearerAccessToken("access_token_value", 3600, new Scope("openid")),
+ new RefreshToken("refresh_token_value"));
+ Mockito.when(tokenResponse.getOIDCTokens()).thenReturn(tokens);
+ Mockito.when(tokenResponse.getTokens()).thenReturn(tokens);
+ Mockito.when(tokenResponse.indicatesSuccess()).thenReturn(true);
}
@Test
public void testSuccesfulTokenResponse() throws Exception {
- action.setTokenResponseContextLookupStrategy(prc -> {
- final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
- final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
- .issuer("https://op.example.com")
- .audience("https://rp.example.com")
- .subject("joe")
- .claim("family_name", "blogs")
- .build();
- atrc.setIdToken(new PlainJWT(claimsSet));
- atrc.setRawTokenResponse(claimsSet.toJSONObject());
- atrc.getRawTokenResponse().put("access_token", "access-token");
- atrc.getRawTokenResponse().put("token_type","Bearer");
- atrc.getRawTokenResponse().put("expires_in","3600");
- atrc.getRawTokenResponse().put("id_token",new PlainJWT(claimsSet).serialize());
- return atrc;
- });
+ action.setTokenResponseContextLookupStrategy(prc -> atrc);
action.initialize();
final Event event = action.execute(src);
@@ -66,68 +102,26 @@ public class ValidateOAuthAccessTokenResponseTest extends AbstractOIDCTest {
}
@Test
- public void testError_NoAccessToken() throws Exception {
- action.setTokenResponseContextLookupStrategy(prc -> {
- final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
- final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
- .issuer("https://op.example.com")
- .audience("https://rp.example.com")
- .subject("joe")
- .claim("family_name", "blogs")
- .build();
- atrc.setIdToken(new PlainJWT(claimsSet));
- atrc.setRawTokenResponse(claimsSet.toJSONObject());
- atrc.getRawTokenResponse().put("token_type","Bearer");
- atrc.getRawTokenResponse().put("expires_in","3600");
- atrc.getRawTokenResponse().put("id_token",new PlainJWT(claimsSet).serialize());
- return atrc;
- });
+ public void testSuccesfulTokenResponse_NoExpiry() throws Exception {
+ tokens = new OIDCTokens(new PlainJWT(claimsSet),
+ new BearerAccessToken("access_token_value", 0, new Scope("openid")),
+ new RefreshToken("refresh_token_value"));
+ Mockito.when(tokenResponse.getOIDCTokens()).thenReturn(tokens);
+ Mockito.when(tokenResponse.getTokens()).thenReturn(tokens);
+ action.setTokenResponseContextLookupStrategy(prc -> atrc);
action.initialize();
final Event event = action.execute(src);
-
- assertEquals(event.getId(), OidcEventIds.INVALID_ACCESS_TOKEN);
- }
-
- @Test
- public void testError_NoIDToken() throws Exception {
- action.setTokenResponseContextLookupStrategy(prc -> {
- final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
- final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
- .issuer("https://op.example.com")
- .audience("https://rp.example.com")
- .subject("joe")
- .claim("family_name", "blogs")
- .build();
- atrc.setIdToken(new PlainJWT(claimsSet));
- atrc.setRawTokenResponse(claimsSet.toJSONObject());
- atrc.getRawTokenResponse().put("access_token", "access-token");
- atrc.getRawTokenResponse().put("token_type","Bearer");
- atrc.getRawTokenResponse().put("expires_in","3600");
- return atrc;
- });
- action.initialize();
- final Event event = action.execute(src);
-
- assertEquals(event.getId(), OidcEventIds.INVALID_ACCESS_TOKEN);
+ //Null event means response valid
+ assertNull(event);
}
+
@Test
- public void testError_NoTokenType() throws Exception {
+ public void testError_NoTokenResponse() throws Exception {
action.setTokenResponseContextLookupStrategy(prc -> {
- final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
- final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
- .issuer("https://op.example.com")
- .audience("https://rp.example.com")
- .subject("joe")
- .claim("family_name", "blogs")
- .build();
- atrc.setIdToken(new PlainJWT(claimsSet));
- atrc.setRawTokenResponse(claimsSet.toJSONObject());
- atrc.getRawTokenResponse().put("access_token", "access-token");
- atrc.getRawTokenResponse().put("expires_in","3600");
- atrc.getRawTokenResponse().put("id_token",new PlainJWT(claimsSet).serialize());
+ final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
return atrc;
});
@@ -138,62 +132,53 @@ public class ValidateOAuthAccessTokenResponseTest extends AbstractOIDCTest {
}
@Test
- public void testError_NoRawTokenResponse() throws Exception {
- action.setTokenResponseContextLookupStrategy(prc -> {
- final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
- return atrc;
- });
+ public void testError_WrongTokenType() throws Exception {
+ tokens = new OIDCTokens(new PlainJWT(claimsSet),
+ new DPoPAccessToken("access_token_value", 0, new Scope("openid")),
+ new RefreshToken("refresh_token_value"));
+ Mockito.when(tokenResponse.getOIDCTokens()).thenReturn(tokens);
+ Mockito.when(tokenResponse.getTokens()).thenReturn(tokens);
+ action.setTokenResponseContextLookupStrategy(prc -> atrc);
action.initialize();
final Event event = action.execute(src);
-
+ assertNotNull(event);
assertEquals(event.getId(), OidcEventIds.INVALID_ACCESS_TOKEN);
}
@Test
- public void testError_WrongTokenType() throws Exception {
- action.setTokenResponseContextLookupStrategy(prc -> {
- final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
- final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
- .issuer("https://op.example.com")
- .audience("https://rp.example.com")
- .subject("joe")
- .claim("family_name", "blogs")
- .build();
- atrc.setIdToken(new PlainJWT(claimsSet));
- atrc.setRawTokenResponse(claimsSet.toJSONObject());
- atrc.getRawTokenResponse().put("access_token", "access-token");
- atrc.getRawTokenResponse().put("token_type","WrongType");
- atrc.getRawTokenResponse().put("expires_in","3600");
- atrc.getRawTokenResponse().put("id_token",new PlainJWT(claimsSet).serialize());
- return atrc;
- });
+ public void testError_TokenExpired() throws Exception {
+ tokens = new OIDCTokens(new PlainJWT(claimsSet),
+ new BearerAccessToken("access_token_value", 1, new Scope("openid")),
+ new RefreshToken("refresh_token_value"));
+ Mockito.when(tokenResponse.getOIDCTokens()).thenReturn(tokens);
+ Mockito.when(tokenResponse.getTokens()).thenReturn(tokens);
+ Mockito.when(atrc.getTokenResponseCreatedAt()).thenReturn(Instant.now().minus(Duration.ofSeconds(10)));
+ action.setTokenResponseContextLookupStrategy(prc -> atrc);
+
action.initialize();
final Event event = action.execute(src);
-
+ assertNotNull(event);
assertEquals(event.getId(), OidcEventIds.INVALID_ACCESS_TOKEN);
}
+ /**
+ * Fake an error response using a successful token response. Otherwise we can not set the wrong type on the context.
+ *
+ * @throws Exception on error
+ */
@Test
public void testError_ErrorResponse() throws Exception {
- action.setTokenResponseContextLookupStrategy(prc -> {
- final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
- final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
- .issuer("https://op.example.com")
- .audience("https://rp.example.com")
- .subject("joe")
- .claim("family_name", "blogs")
- .build();
- atrc.setIdToken(new PlainJWT(claimsSet));
- atrc.setRawTokenResponse(claimsSet.toJSONObject());
- atrc.getRawTokenResponse().put("error", "invalid_request");
- return atrc;
- });
+ final OIDCTokenResponse tokenErrorResponse = Mockito.mock(OIDCTokenResponse.class);
+ Mockito.when(tokenErrorResponse.indicatesSuccess())
+ .thenReturn(false);
+ Mockito.when(atrc.getTokenResponse()).thenReturn(tokenErrorResponse);
+ action.setTokenResponseContextLookupStrategy(prc -> atrc);
action.initialize();
final Event event = action.execute(src);
-
+ assertNotNull(event);
assertEquals(event.getId(), OidcEventIds.INVALID_ACCESS_TOKEN);
}
diff --git a/pom.xml b/pom.xml
index f74f974..79b06e1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -125,7 +125,7 @@
<artifactId>java-support</artifactId>
<version>${java-support.version}</version>
<scope>provided</scope>
- </dependency>
+ </dependency>
<!-- Test bom dependencies -->
<dependency>
<groupId>${idp.groupId}</groupId>
@@ -152,6 +152,13 @@
<version>${oidc.common.version}</version>
<type>test-jar</type>
<scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp.plugin.authn</groupId>
+ <artifactId>idp-plugin-oidc-rp-api</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ <type>test-jar</type>
</dependency>
</dependencies>
</dependencyManagement>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list