[java-idp-plugin-oidc-rp] branch main updated: Allow a setting to force only JWT UserInfo response type support
Phil Smart
philip.smart at jisc.ac.uk
Fri Oct 21 13:49:58 UTC 2022
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=a28c8afb8285f0893c91b2cbc97cee2d59f484f8
The following commit(s) were added to refs/heads/main by this push:
new a28c8af Allow a setting to force only JWT UserInfo response type support
a28c8af is described below
commit a28c8afb8285f0893c91b2cbc97cee2d59f484f8
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Oct 21 14:49:53 2022 +0100
Allow a setting to force only JWT UserInfo response type support
- Add and clean tests
- Disallow plain id_token types - although this was already the case,
this makes it explicit.
---
.../impl/DefaultUserInfoResponseDecoder.java | 41 +++-
.../oidc/rp/impl/ExtractIDTokenFromResponse.java | 9 +-
.../authn/oidc/rp/impl/UserInfoEndpointLookup.java | 64 ++++++-
.../idp/service/relying-party/postconfig.xml | 1 +
.../authn/oidc/rp/conf/authn/oidc-rp.properties | 1 +
.../impl/DefaultUserInfoResponseDecoderTest.java | 212 +++++++++++++++++++++
.../authn/oidc/rp/impl/AbstractOIDCTest.java | 43 ++++-
.../rp/impl/ExtractIDTokenFromResponseTest.java | 23 +++
8 files changed, 377 insertions(+), 17 deletions(-)
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 2cf6b8b..f66bd4a 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
@@ -21,6 +21,7 @@ import java.util.Map;
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;
@@ -41,12 +42,20 @@ 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.*/
-//TODO check no bypass for plain to be recorded for a JWT type, as no signature check would then be performed
+/**
+ * Response decoder for UserInfo responses. Supports both plain JSON Object and JWT responses. Importantly,
+ * the decoder *must not ever* decode a JWT response as a plain response type, otherwise the signature check
+ * may not be performed downstream - although other validation for the plain object type should. That is, we
+ * can not rely solely on the content-type header in-case of content-type header injection attacks — the logic
+ * that builds either the JWT or plain response should fail, or at least present an invalid UserInfo response token.
+ */
public class DefaultUserInfoResponseDecoder extends AbstractJSONResponseDecoderFunction<UserInfoResponse> {
/** The application/jwt media type.*/
- @Nonnull private static final MediaType APPLICATION_JWT = new MediaType("application", "jwt");
+ @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";
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(DefaultUserInfoResponseDecoder.class);
@@ -58,14 +67,25 @@ public class DefaultUserInfoResponseDecoder extends AbstractJSONResponseDecoderF
try {
final int httpStatusCode = httpResponse.getStatusLine().getStatusCode();
- if (httpStatusCode != HttpStatus.SC_OK) {
+ 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("HTTP endpoint returned a Non-ok message of '{}'",errorContent);
+ log.error("Non-ok status code ({}) returned from UserInfo HTTP endpoint, error is: '{}' ",
+ httpStatusCode, errorContent);
+
+ } else 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);
+
+ } else {
+ log.warn("Non-ok status code ({}) returned from UserInfo HTTP endpoint", httpStatusCode);
}
- log.warn("Non-ok status code ({}) returned from HTTP endpoint: {}", httpStatusCode,
- httpResponse.getStatusLine().getReasonPhrase());
return null;
} else if (httpResponse.getEntity() == null || httpResponse.getEntity().getContent() == null) {
log.warn("HTTP response does not contain a message entity, nothing to decode");
@@ -83,10 +103,12 @@ public class DefaultUserInfoResponseDecoder extends AbstractJSONResponseDecoderF
// 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.isTraceEnabled()) {
- log.trace("UserInfo response decoder parsed a {} JWT type",
+ log.trace("UserInfo response decoder parsed an {} JWT type",
parsedJwt instanceof SignedJWT ? "Signed" :
(parsedJwt instanceof EncryptedJWT ? "Encrypted" : "plain"));
}
@@ -95,6 +117,7 @@ public class DefaultUserInfoResponseDecoder extends AbstractJSONResponseDecoderF
} else if (MediaType.APPLICATION_JSON.compareTo(MimeType.valueOf(contentType.getMimeType())) == 0){
+ // 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();
@@ -108,7 +131,7 @@ public class DefaultUserInfoResponseDecoder extends AbstractJSONResponseDecoderF
}
} catch (final Exception e) {
- log.warn("Unable to decode response", e);
+ log.warn("Unable to decode UserInfo response", e);
return null;
}
return null;
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
index b6d7557..8a62c6e 100644
--- 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
@@ -35,7 +35,6 @@ import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.PlainObject;
import com.nimbusds.jwt.EncryptedJWT;
-import com.nimbusds.jwt.PlainJWT;
import com.nimbusds.jwt.SignedJWT;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
@@ -48,7 +47,8 @@ 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}.
+ * {@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}
@@ -135,8 +135,9 @@ public class ExtractIDTokenFromResponse extends AbstractProfileAction {
try {
final JOSEObject joseObject = JOSEObject.parse(rawIdTokenValue);
if (joseObject instanceof PlainObject) {
- log.trace("{} Plain id_token found", getLogPrefix());
- responseCtx.setIdToken(PlainJWT.parse(rawIdTokenValue));
+ 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());
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 cd688a7..39bcd47 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
@@ -20,6 +20,7 @@ package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
import java.util.function.Function;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.action.ActionSupport;
@@ -35,6 +36,10 @@ 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.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse.UserInfoResponseType;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.oidc.profile.config.OIDCAuthorizationConfiguration;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -56,12 +61,31 @@ public class UserInfoEndpointLookup extends AbstractHttpOIDCAuthenticationAction
@Nonnull private Function<ProfileRequestContext, UserInfoResponseContext>
userInfoResponseContextLookupStrategy;
+ /** Lookup function for relying party context. */
+ @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+
+ /** Applicable stashed profile configuration. */
+ @Nullable private OIDCAuthorizationConfiguration profileConfiguration;
+
/** Constructor.*/
public UserInfoEndpointLookup() {
userInfoResponseContextLookupStrategy =
new ChildContextLookup<>(UserInfoResponseContext.class, true).compose(
new InboundMessageContextLookup());
+
+ relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+ }
+
+ /**
+ * Set lookup strategy for relying party context.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setRelyingPartyContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+ relyingPartyContextLookupStrategy =
+ Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
}
/**
@@ -76,6 +100,28 @@ public class UserInfoEndpointLookup extends AbstractHttpOIDCAuthenticationAction
userInfoResponseContextLookupStrategy = Constraint.isNotNull(strategy,
"UserInfoResponseContext lookup strategy cannot be null");
}
+
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+ return false;
+ }
+
+ final RelyingPartyContext rpCtx = relyingPartyContextLookupStrategy.apply(profileRequestContext);
+ if (rpCtx != null && rpCtx.getConfiguration() != null &&
+ rpCtx.getProfileConfig() instanceof OIDCAuthorizationConfiguration) {
+ profileConfiguration = (OIDCAuthorizationConfiguration) rpCtx.getProfileConfig();
+ }
+ if (profileConfiguration == null) {
+ log.error("{} OIDCAuthorizationConfiguration not found", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ }
+
+ return true;
+ }
@Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final AuthenticationContext authenticationContext) {
@@ -90,9 +136,21 @@ public class UserInfoEndpointLookup extends AbstractHttpOIDCAuthenticationAction
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return;
}
-
- try {
- userInfoCtx.setUserInfo(handleRequest(profileRequestContext));
+
+ try {
+
+ final UserInfoResponse response = handleRequest(profileRequestContext);
+
+ final boolean requireJWTType =
+ profileConfiguration.requireJWTUserInfoResponses(profileRequestContext);
+
+ if (requireJWTType && response.getType() == UserInfoResponseType.PLAIN) {
+ log.error("{} JWT UserInfo response required, but plain JSON Object returned",getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+ return;
+ }
+
+ userInfoCtx.setUserInfo(response);
} catch (final OIDCRPException e) {
log.error("{} Unable to return claims from UserInfo endpoint",getLogPrefix(),e);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 6e4dc3b..4d61a81 100644
--- a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -30,6 +30,7 @@
p:tokenEndpointAuthMethods="%{idp.authn.oidc.rp.client.authenticationMethod:client_secret_basic}"
p:responseMode="%{idp.authn.oidc.rp.client.responseMode:#{null}}"
p:retrieveUserInfoEndpointClaims="%{idp.authn.oidc.rp.client.userinfo.enabled:true}"
+ p:requireJWTUserInfoResponses="%{idp.authn.oidc.rp.client.userinfo.requireJWTResponse:false}"
p:redirectUriOverride="%{idp.authn.oidc.rp.client.redirectURI:#{null}}"
p:encryptRequestObject="%{idp.authn.oidc.rp.client.requestobject.encrypted:false}"
p:signRequestObject="%{idp.authn.oidc.rp.client.requestobject.signed:true}"
diff --git a/idp-oidc-rp-impl/src/main/resources/net/shibboleth/idp/plugin/authn/oidc/rp/conf/authn/oidc-rp.properties b/idp-oidc-rp-impl/src/main/resources/net/shibboleth/idp/plugin/authn/oidc/rp/conf/authn/oidc-rp.properties
index 42c90f7..3b769db 100644
--- a/idp-oidc-rp-impl/src/main/resources/net/shibboleth/idp/plugin/authn/oidc/rp/conf/authn/oidc-rp.properties
+++ b/idp-oidc-rp-impl/src/main/resources/net/shibboleth/idp/plugin/authn/oidc/rp/conf/authn/oidc-rp.properties
@@ -17,6 +17,7 @@ idp.authn.oidc.rp.client.redirecturl.allowedOrigins = https://localhost:8443
#idp.authn.oidc.rp.client.requestobject.signed = true
#idp.authn.oidc.rp.client.userinfo.enabled = true
+#idp.authn.oidc.rp.client.userinfo.requireJWTResponse = false
## Use small fetch interval so we can re-run tests against the certification OP
#idp.authn.oidc.rp.provider.keyfetch.interval = PT30M
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
new file mode 100644
index 0000000..859d7cf
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoderTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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 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;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.message.BasicHeader;
+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 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}.*/
+public class DefaultUserInfoResponseDecoderTest extends AbstractOIDCTest {
+
+ /** The decoder to test.*/
+ private DefaultUserInfoResponseDecoder decoder;
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception {
+ super.setup();
+ decoder = new DefaultUserInfoResponseDecoder();
+ decoder.setObjectMapper(new ObjectMapper());
+ }
+
+ @Test
+ public void testEncoder_InternalServerErrorResponse() throws ComponentInitializationException {
+ decoder.initialize();
+ final HttpResponse response = Mockito.mock(HttpResponse.class);
+ final StatusLine mockStatusLine = Mockito.mock(StatusLine.class);
+ when(mockStatusLine.getStatusCode()).thenReturn(500);
+ when(response.getStatusLine()).thenReturn(mockStatusLine);
+
+ final UserInfoResponse userInfoResponse = decoder.apply(response);
+ assertNull(userInfoResponse);
+
+ }
+
+ @Test
+ public void testEncoder_NoMessageEntity() throws ComponentInitializationException {
+ decoder.initialize();
+ final HttpResponse response = Mockito.mock(HttpResponse.class);
+ final StatusLine mockStatusLine = Mockito.mock(StatusLine.class);
+ when(mockStatusLine.getStatusCode()).thenReturn(200);
+ when(response.getStatusLine()).thenReturn(mockStatusLine);
+ when(response.getEntity()).thenReturn(null);
+
+ final UserInfoResponse userInfoResponse = decoder.apply(response);
+ assertNull(userInfoResponse);
+ }
+
+
+ @Test
+ public void testEncoder_PlainResponse_Success() 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);
+ when(mockHttpEntity.getContentType()).thenReturn(
+ new BasicHeader("Content-Type",MediaType.APPLICATION_JSON_VALUE));
+ when(mockHttpEntity.getContent()).thenReturn(new ByteArrayInputStream(USERINFO_RESPONSE.getBytes()));
+ final UserInfoResponse userInfoResponse = decoder.apply(response);
+
+ assertNotNull(userInfoResponse);
+ assertTrue(userInfoResponse.getType() == UserInfoResponseType.PLAIN);
+ assertNotNull(userInfoResponse.getClaimsSet());
+ assertEquals(userInfoResponse.getSub(), "248289761001");
+
+ }
+
+ /*
+ * This should fail, otherwise you might be able to 'claim' the JWT response
+ * is a plain JSON object, which would skip signature validation.
+ */
+ @Test
+ public void testEncoder_JWSResponse_PlainJSONContentType_Fail() 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",MediaType.APPLICATION_JSON_VALUE));
+
+ when(mockHttpEntity.getContent()).thenReturn(new ByteArrayInputStream(USERINFO_RESPONSE_JWS.getBytes()));
+ final UserInfoResponse userInfoResponse = decoder.apply(response);
+
+ assertNull(userInfoResponse);
+ }
+
+ @Test
+ public void testEncoder_PlainResponse_JWTContentType_Fail() 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",DefaultUserInfoResponseDecoder.APPLICATION_JWT.toString()));
+
+ when(mockHttpEntity.getContent()).thenReturn(new ByteArrayInputStream(USERINFO_RESPONSE.getBytes()));
+ final UserInfoResponse userInfoResponse = decoder.apply(response);
+
+ assertNull(userInfoResponse);
+ }
+
+ @Test
+ public void testEncoder_JWSResponse_Success() 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);
+ when(mockHttpEntity.getContentType()).thenReturn(
+ new BasicHeader("Content-Type",DefaultUserInfoResponseDecoder.APPLICATION_JWT.toString()));
+ when(mockHttpEntity.getContent()).thenReturn(new ByteArrayInputStream(USERINFO_RESPONSE_JWS.getBytes()));
+ 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());
+ }
+
+ @Test
+ public void testEncoder_ErrorResponse() 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(401);
+ 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);
+
+ final UserInfoResponse userInfoResponse = decoder.apply(response);
+
+ assertNull(userInfoResponse);
+ }
+
+ @Test
+ public void testEncoder_JWEAndJWSResponse_Success() 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);
+ when(mockHttpEntity.getContentType()).thenReturn(
+ new BasicHeader("Content-Type",DefaultUserInfoResponseDecoder.APPLICATION_JWT.toString()));
+ when(mockHttpEntity.getContent()).thenReturn(new ByteArrayInputStream(USERINFO_RESPONSE_JWE.getBytes()));
+ final UserInfoResponse userInfoResponse = decoder.apply(response);
+
+ assertNotNull(userInfoResponse);
+ assertTrue(userInfoResponse.getType() == UserInfoResponseType.JWT);
+ assertTrue(userInfoResponse.isEncrypted());
+ assertFalse(userInfoResponse.isClaimsSetAvailable());
+ }
+
+}
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 e207d2e..7badac4 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
@@ -117,7 +117,30 @@ public abstract class AbstractOIDCTest {
+ " \"scope\": \"openid\"\n"
+ "}";
- /** Mock response from the UserInfo endpoint.*/
+ /** Mock a UserInfo JWE reponse.*/
+ @Nonnull @NotEmpty
+ protected final String USERINFO_RESPONSE_JWE = "eyJraWQiOiIzNGUzNWJiZS1iZDBiLTQyMzEtYWJlMC04Y2Z"
+ + "lOGE0N2E4ZDYiLCJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIn0..hCurqMIiAYyW4JO"
+ + "X.qehtZXxgEAVsBu2sci0C9hk74ZsFG6VUvzOmDI16eHZWpT68iDrgfy_kfc6T0UUNFcv31crLMX5etuURgfJ"
+ + "HKcdiXciMuF3YAfkUp0kmGZCjyNoFflnLziVjDwBpJ9Gighr24dSKfVsFV0uo9gI1qZJdWyqzWS_dZPf91Klh"
+ + "7ydnIuWmvi4AVC35RCn63weYXeyxSB0fkZdQylTMMHyZEMqfoqcFOjnMBnmgAOHvNHJsBxGAMXh05cWJ7uBMz"
+ + "8LwWJjPiS2e2L2irzmi8yPQXYkIpLGs4aX06nrBSniMzZi2KC9wFYk3PSvS_iHR-9arwFEG3u8QK69GmY9_Plh"
+ + "Z_e5Nd6PEYWqsRzz-QRlM3oOk7jJ1k4CM1MxpXPRQSkNFl1RI7d94hwixaTW07bkbmf090e-CzwjcxNUfKn1WA"
+ + "zLsNvP8V1Lhk1gG0ZHUIrPtY6r469yIHaW2QWHJ-eTT6sddZkJyqhcIYhYkvig2-r3ecxcJVgtMFtq06qVErE1"
+ + "UaYq1Aqk4P3dJ7qeRVToFP8boB8pqwvw1844zLdjsrqEBdCjOx6-Y_ADn0UiTvTpua7UmeeYRv3lRLerwFWaX1"
+ + "ox60t3UsMMrRobwq9rYKMkqtEiZuCCTMqlRZR2vx51upfGPRV1uSy81u5IVCYJWH6x9b47DaztH1UercIrCSll"
+ + "idfVvCqIktagyV_tTkGNRLqPHnfXfd3lq1ZdbIChgxK2BdI2IghDqpUkeCA6DxWoEfGefPJv8x7-ryi1bn3jV4"
+ + "65F8uJOr90VHcyZHLLQmjc1g6vW7Kw8nPsuM7YzczD8fCaa0B4RjkhR5zB8zsoTci3AC8CIxV0Z1F5G0SFpgPA"
+ + "jw4cVTeI6CD7PXWheq1zCLkzcLIDZ-mpr8FHazP7KHXgE5uiDbdx0TyfB583W4A3N6KOIlmQsGOAsSfUMP6fvE"
+ + "c-g5m-Y0d3RKRvS3VdYXF2GF7ThUL7r9KIsDpBzo5-QtdZhEMs20jIcJV-CEIKWo1vb0hhDHAQECytRjm72F6T"
+ + "-GBhIwEv8Uzd9EWo5TWYgNINU9kkJPLPCHMoI8lYaSgwgfj9viTSTaWPFP_nxvwryZObDmxsuXwykbFte1RpQGz"
+ + "BYyzqUWECEdUbHixqRx5xKdRx3vCMAHScO77p58sD9s4W58FI9yg7pQKzB-dL_m0vVvfzdg4FqvwiN9H-iNTGHV"
+ + "KH82Uduf0v64Ms7hTB7bgquzSxpvfFCbRbwBQ3dEzCDPh73ddPrnWRXIi6FocNKbqhsamrmyXYXcebOH4jwSeN2"
+ + "jFrAtOoMcA-mheBCQQp11_vKKnXL3PR0y5oZQse7kobq15Y6wFSz35fk2mRMhUXuCsAAhNfSmLm0X1e687N4Z1E"
+ + "83mYF8Tu_Vc2zUT9qMDVG_CR0y-pW7PHGafg4F0v485Kk0fcTZZtDGjVV-Qmnpp88fgNn6urKXEq3yg.YSFF9_9"
+ + "WJctzi7aWeMkaXg";
+
+ /** Mock Plain response from the UserInfo endpoint.*/
@Nonnull @NotEmpty
protected final String USERINFO_RESPONSE ="{\n"
+ " \"sub\": \"248289761001\",\n"
@@ -129,6 +152,24 @@ public abstract class AbstractOIDCTest {
+ " \"picture\": \"http://example.com/janedoe/me.jpg\"\n"
+ " }";
+ /** Mock a UserInfo JWS reponse.*/
+ protected final String USERINFO_RESPONSE_JWS =
+ " eyJraWQiOiIwMDk5MmNlNi0wMDNjLTQzOGItODI1OS05Mjc3ZWUwYWZmNjgiLCJhbGciOiJ"
+ + "SUzI1NiJ9.eyJzdWIiOiJ1c2VyLXN1YmplY3QtMTIzNDUzMSIsIndlYnNpdGUiOiJodHRw"
+ + "czpcL1wvb3BlbmlkLm5ldFwvIiwiem9uZWluZm8iOiJBbWVyaWNhXC9Mb3NfQW5nZWxlcy"
+ + "IsImJpcnRoZGF0ZSI6IjIwMDAtMDItMDMiLCJnZW5kZXIiOiJmZW1hbGUiLCJwcm9maWxlI"
+ + "joiaHR0cHM6XC9cL2V4YW1wbGUuY29tXC91c2VyIiwiaXNzIjoiaHR0cHM6XC9cL3d3dy5j"
+ + "ZXJ0aWZpY2F0aW9uLm9wZW5pZC5uZXRcL3Rlc3RcL2FcL3Rlc3RfcnBfcHJveHlcLyIsInB"
+ + "yZWZlcnJlZF91c2VybmFtZSI6ImQudHUiLCJnaXZlbl9uYW1lIjoiRGVtbyIsIm1pZGRsZV"
+ + "9uYW1lIjoiVGhlcmVzYSIsImxvY2FsZSI6ImVuLVVTIiwiYXVkIjoibXl0ZXN0Y2xpZW50I"
+ + "iwidXBkYXRlZF9hdCI6MTU4MDAwMDAwMCwibmFtZSI6IkRlbW8gVC4gVXNlciIsIm5pY2tu"
+ + "YW1lIjoiRGVlIiwiZmFtaWx5X25hbWUiOiJVc2VyIn0.K8qvXHwxbGlLvvXp8l4FyU84ZQro"
+ + "D5QgGG5uC58DZ5NyDLrcj9lMVEDaVX6pfkLMXtqFrnAuO-ZidXaV1n2loBi1cwDTy-IeeoKH"
+ + "h81aC-nsVZdp5JYl-R-i56tu-kAC9Yw8-Ogm9MlWufVW-0iZC_HhJxho8FI63MupPd1OJX169"
+ + "sWz7qsKAB3aJauax_ekNyOXTdhNdduQ4DmmKTHKW5tJKtM7gimE4bY2zEIZCEz0OGyVKzkOng"
+ + "Z3MvlRxpbMwo9wC_4UO-dhEu6UIv8rdTNfqqOmuqH4cJqi7Xa2ZI4JtuXe9bdvkKra1PgZIq0s"
+ + "SbfVNtxauecCzVGzQUAIpg";
+
/** Client metadata.*/
@Nonnull protected final String CLIENT_METADATA = "[\n"
+ " {\n"
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
index 5fad162..5cd9818 100644
--- 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
@@ -17,6 +17,8 @@
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;
@@ -29,6 +31,7 @@ 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.*/
@@ -37,6 +40,7 @@ public class ExtractIDTokenFromResponseTest extends AbstractOIDCTest {
private ExtractIDTokenFromResponse action;
+ @Override
@BeforeMethod
public void setup() throws Exception {
super.setup();
@@ -75,6 +79,25 @@ public class ExtractIDTokenFromResponseTest extends AbstractOIDCTest {
}
+ /* 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?
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list