CVE-2021-44878 (published 2022-01-06T13:15Z)

If an OpenID Connect provider supports the "none" algorithm (i.e., tokens with no signature), pac4j v5.3.0 (and prior) does not refuse it without an explicit configuration on its side or for the "idtoken" response type which is not secure and violates the OpenID Core Specification. The "none" algorithm does not require any signature verification when validating the ID tokens, which allows the attacker to bypass the token validation by injecting a malformed ID token using "none" as the value of "alg" key in the header with an empty signature value.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-347
cwe_name: Improper Verification of Cryptographic Signature
description: The product does not verify, or incorrectly verifies, the cryptographic signature for data.
extended_description: 
            
            
         
         

Fix information:
repo_url: https://github.com/pac4j/pac4j
commit_details: repo_url: https://github.com/pac4j/pac4j
author: Jérôme LELEU
author_date: 2021-12-06 09:09:36+01:00
msg: reinforce security on OIDC
repository: repo_url: https://github.com/pac4j/pac4j
repo_name: pac4j/pac4j
description: Security engine for Java (authentication, authorization, multi frameworks): OAuth, CAS, SAML, OpenID Connect, LDAP, JWT...
repo_language: Java
owner: pac4j
file_changes: filename: openid-connect.md
change_type: ModificationType.MODIFY
diff: @@ -145,3 +145,9 @@ The additional param `TokenExpirationAdvance` allows to set the time in seconds,
 ```java
 config.setTokenExpirationAdvance(10);
 ```
+
+Since version 5.2 and to reinforce security, the `none` alogithm for ID tokens (meaning no signature validation) must be explictly accepted by using:
+
+```java
+config.setAllowUnsignedIdTokens(true);
+```

programming_language: Markdown
filename: release-notes.md
change_type: ModificationType.MODIFY
diff: @@ -6,6 +6,7 @@ title: Release notes&#58;
 **v5.2**:
 
 - The JEE core components are now in the `pac4j-jee` dependency (and no longer in the `pac4j-core` dependency)
+- Reinforce security on the OIDC protocol support: the `none` algorithm must be explicitly accepted on client side (`allowUnsignedIdTokens`)
 
 **v5.1.5**:
 

programming_language: Markdown
filename: OidcConfiguration.java
change_type: ModificationType.MODIFY
diff: @@ -140,6 +140,8 @@ public class OidcConfiguration extends BaseClientConfiguration {
 
     private TokenValidator tokenValidator;
 
+    private boolean allowUnsignedIdTokens;
+
     @Override
     protected void internalInit() {
         // checks
@@ -486,6 +488,14 @@ public class OidcConfiguration extends BaseClientConfiguration {
         this.mappedClaims = mappedClaims;
     }
 
+    public boolean isAllowUnsignedIdTokens() {
+        return allowUnsignedIdTokens;
+    }
+
+    public void setAllowUnsignedIdTokens(final boolean allowUnsignedIdTokens) {
+        this.allowUnsignedIdTokens = allowUnsignedIdTokens;
+    }
+
     @Override
     public String toString() {
         return toNiceString(this.getClass(), "clientId", clientId, "secret", "[protected]",
@@ -495,6 +505,6 @@ public class OidcConfiguration extends BaseClientConfiguration {
             "connectTimeout", connectTimeout, "readTimeout", readTimeout, "resourceRetriever", resourceRetriever,
             "responseType", responseType, "responseMode", responseMode, "logoutUrl", logoutUrl,
             "withState", withState, "stateGenerator", stateGenerator, "logoutHandler", logoutHandler,
-            "tokenValidator", tokenValidator, "mappedClaims", mappedClaims);
+            "tokenValidator", tokenValidator, "mappedClaims", mappedClaims, "allowUnsignedIdTokens", allowUnsignedIdTokens);
     }
 }

programming_language: Java
filename: UserInfoOidcAuthenticator.java
change_type: ModificationType.MODIFY
diff: @@ -6,8 +6,6 @@ import static org.pac4j.core.profile.AttributeLocation.PROFILE_ATTRIBUTE;
 import java.io.IOException;
 import java.util.Map;
 
-import javax.naming.AuthenticationException;
-
 import org.pac4j.core.context.WebContext;
 import org.pac4j.core.context.session.SessionStore;
 import org.pac4j.core.credentials.Credentials;
@@ -99,9 +97,8 @@ public class UserInfoOidcAuthenticator extends InitializableObject implements Au
                 httpResponse.getContent());
             final var userInfoResponse = UserInfoResponse.parse(httpResponse);
             if (userInfoResponse instanceof UserInfoErrorResponse) {
-                logger.error("Bad User Info response, error={}",
-                    ((UserInfoErrorResponse) userInfoResponse).getErrorObject().toJSONObject());
-                throw new AuthenticationException();
+                throw new TechnicalException("Bad User Info response, error="
+                    + ((UserInfoErrorResponse) userInfoResponse).getErrorObject().toJSONObject());
             } else {
                 final var userInfoSuccessResponse = (UserInfoSuccessResponse) userInfoResponse;
                 final JWTClaimsSet userInfoClaimsSet;
@@ -112,7 +109,7 @@ public class UserInfoOidcAuthenticator extends InitializableObject implements Au
                 }
                 return userInfoClaimsSet;
             }
-        } catch (IOException | ParseException | java.text.ParseException | AuthenticationException e) {
+        } catch (IOException | ParseException | java.text.ParseException e) {
             throw new TechnicalException(e);
         }
     }

programming_language: Java
filename: TokenValidator.java
change_type: ModificationType.MODIFY
diff: @@ -51,13 +51,13 @@ public class TokenValidator {
         final var _clientID = new ClientID(configuration.getClientId());
 
         for (var jwsAlgorithm : jwsAlgorithms) {
-            if ("none".equals(jwsAlgorithm.getName())) {
-                jwsAlgorithm = null;
-            }
-
             // build validator
             final IDTokenValidator idTokenValidator;
-            if (jwsAlgorithm == null) {
+            if ("none".equals(jwsAlgorithm.getName())) {
+                if (!configuration.isAllowUnsignedIdTokens()) {
+                    throw new TechnicalException("Unsigned ID tokens are not allowed");
+                }
+                logger.warn("Allowing unsigned ID tokens");
                 idTokenValidator = new IDTokenValidator(configuration.findProviderMetadata().getIssuer(), _clientID);
             } else if (CommonHelper.isNotBlank(configuration.getSecret()) && (JWSAlgorithm.HS256.equals(jwsAlgorithm) ||
                 JWSAlgorithm.HS384.equals(jwsAlgorithm) || JWSAlgorithm.HS512.equals(jwsAlgorithm))) {
@@ -112,4 +112,9 @@ public class TokenValidator {
             throw new TechnicalException("Unable to validate the ID token");
         }
     }
+
+    // for tests
+    List<IDTokenValidator> getIdTokenValidators() {
+        return idTokenValidators;
+    }
 }

programming_language: Java
filename: TokenValidatorTests.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,88 @@
+package org.pac4j.oidc.profile.creator;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
+import org.junit.Before;
+import org.junit.Test;
+import org.pac4j.core.exception.TechnicalException;
+import org.pac4j.core.util.TestsConstants;
+import org.pac4j.core.util.TestsHelper;
+import org.pac4j.oidc.config.OidcConfiguration;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.mockito.Mockito.*;
+import static org.junit.Assert.*;
+
+/**
+ * Tests {@link TokenValidator}.
+ *
+ * @author Jerome LELEU
+ * @since 5.2.0
+ */
+public final class TokenValidatorTests implements TestsConstants {
+
+    private OidcConfiguration configuration;
+
+    private List<JWSAlgorithm> algorithms;
+
+    @Before
+    public void setUp() throws URISyntaxException {
+        configuration = mock(OidcConfiguration.class);
+        final OIDCProviderMetadata metadata = mock(OIDCProviderMetadata.class);
+        when(metadata.getIssuer()).thenReturn(new Issuer(PAC4J_URL));
+        when(metadata.getJWKSetURI()).thenReturn(new URI(PAC4J_BASE_URL));
+        when(configuration.findProviderMetadata()).thenReturn(metadata);
+        when(configuration.getClientId()).thenReturn(ID);
+        when(configuration.getSecret()).thenReturn(SECRET);
+        algorithms = new ArrayList<>();
+        when(metadata.getIDTokenJWSAlgs()).thenReturn(algorithms);
+    }
+
+    @Test
+    public void testNoAlgoDefinedAtProvider() {
+        TestsHelper.expectException(() -> new TokenValidator(configuration), TechnicalException.class,
+            "There must at least one JWS algorithm supported on the OpenID Connect provider side");
+    }
+
+    @Test
+    public void testNoneAlgoNotAllowed() {
+        algorithms.add(JWSAlgorithm.parse("none"));
+        TestsHelper.expectException(() -> new TokenValidator(configuration), TechnicalException.class,
+            "Unsigned ID tokens are not allowed");
+    }
+
+    @Test
+    public void testNoneAlgoAllowed() {
+        algorithms.add(JWSAlgorithm.parse("none"));
+        when(configuration.isAllowUnsignedIdTokens()).thenReturn(true);
+        final TokenValidator validator = new TokenValidator(configuration);
+        final List<IDTokenValidator> validators = validator.getIdTokenValidators();
+        assertEquals(1, validators.size());
+        assertTrue(validators.get(0) instanceof IDTokenValidator);
+    }
+
+    @Test
+    public void testTwoAlgorithms() {
+        algorithms.add(JWSAlgorithm.HS256);
+        algorithms.add(JWSAlgorithm.RS256);
+        final TokenValidator validator = new TokenValidator(configuration);
+        final List<IDTokenValidator> validators = validator.getIdTokenValidators();
+        assertEquals(2, validators.size());
+    }
+
+    @Test
+    public void testTwoAlgorithmsOnePreferred() {
+        algorithms.add(JWSAlgorithm.HS256);
+        algorithms.add(JWSAlgorithm.RS256);
+        when(configuration.getPreferredJwsAlgorithm()).thenReturn(JWSAlgorithm.HS256);
+        final TokenValidator validator = new TokenValidator(configuration);
+        final List<IDTokenValidator> validators = validator.getIdTokenValidators();
+        assertEquals(1, validators.size());
+    }
+}

programming_language: Java