[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