[java-idp-oidc] branch main updated: JOIDC-64 - Refactor client authn on OAuth2 endpoints into a login flow

Scott Cantor cantor.2 at osu.edu
Wed Dec 15 16:55:02 UTC 2021


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch main
in repository java-idp-oidc.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=ada1a4c5d416844deaa09399333d3d70dbe6669c

The following commit(s) were added to refs/heads/main by this push:
     new ada1a4c5 JOIDC-64 - Refactor client authn on OAuth2 endpoints into a login flow
ada1a4c5 is described below

commit ada1a4c5d416844deaa09399333d3d70dbe6669c
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Dec 15 11:54:59 2021 -0500

    JOIDC-64 - Refactor client authn on OAuth2 endpoints into a login flow
    
    https://shibboleth.atlassian.net/browse/JOIDC-64
    
    Client metadata-backed CredentialValidator
---
 .../impl/OIDCClienInfoCredentialValidator.java     | 155 ++++++++++++++++++++
 .../impl/OIDCClienInfoCredentialValidatorTest.java | 157 +++++++++++++++++++++
 .../impl/ValidateClientAuthenticationTypeTest.java |  32 +----
 3 files changed, 313 insertions(+), 31 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/OIDCClienInfoCredentialValidator.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/OIDCClienInfoCredentialValidator.java
new file mode 100644
index 00000000..d124fb95
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/OIDCClienInfoCredentialValidator.java
@@ -0,0 +1,155 @@
+/*
+ * 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.oidc.op.authn.impl;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginException;
+
+import net.shibboleth.idp.authn.AbstractUsernamePasswordCredentialValidator;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.UsernamePasswordContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.utilities.java.support.codec.StringDigester;
+import net.shibboleth.utilities.java.support.codec.StringDigester.OutputFormat;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+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.auth.Secret;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+/**
+ * A password validator that authenticates against OIDC client metadata (which may itself be emulated
+ * via SAML metadata).
+ */
+ at ThreadSafeAfterInit
+public class OIDCClienInfoCredentialValidator extends AbstractUsernamePasswordCredentialValidator {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(OIDCClienInfoCredentialValidator.class);
+    
+    /** Strategy that will return {@link OIDCMetadataContext}. */
+    @Nonnull private Function<ProfileRequestContext,OIDCMetadataContext> oidcMetadataContextLookupStrategy;
+    
+    /** Digester for SHA-1. */
+    @NonnullAfterInit private StringDigester digester;
+    
+    /** Constructor. */
+    public OIDCClienInfoCredentialValidator() {
+        oidcMetadataContextLookupStrategy = new ChildContextLookup<>(OIDCMetadataContext.class).compose(
+                new InboundMessageContextLookup());
+    }
+    
+    /**
+     * Set the strategy used to return the {@link OIDCMetadataContext}.
+     * 
+     * @param strategy The lookup strategy.
+     */
+    public void setOidcMetadataContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,OIDCMetadataContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+    
+        oidcMetadataContextLookupStrategy =
+                Constraint.isNotNull(strategy, "OIDCMetadataContext lookup strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        try {
+            digester = new StringDigester("SHA-256", OutputFormat.BASE64);
+        } catch (final NoSuchAlgorithmException e) {
+            throw new ComponentInitializationException("Error creating digester", e);
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected Subject doValidate(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final UsernamePasswordContext usernamePasswordContext,
+            @Nullable final WarningHandler warningHandler,
+            @Nullable final ErrorHandler errorHandler) throws Exception {
+        
+        OIDCClientInformation clientInformation = null;
+        
+        final OIDCMetadataContext oidcMetadataContext = oidcMetadataContextLookupStrategy.apply(profileRequestContext);
+        if (oidcMetadataContext != null) {
+            clientInformation = oidcMetadataContext.getClientInformation();
+        }
+        
+        if (clientInformation == null) {
+            log.debug("{} OIDC client metadata is missing", getLogPrefix());
+            final LoginException e = new LoginException(AuthnEventIds.UNKNOWN_USERNAME);
+            if (errorHandler != null) { 
+                errorHandler.handleError(profileRequestContext, authenticationContext, e,
+                        AuthnEventIds.UNKNOWN_USERNAME);
+            }
+            throw e;
+        } else if (clientInformation.getSecret() == null) {
+            log.debug("{} OIDC client metadata for '{}' missing client secret", getLogPrefix(),
+                    clientInformation.getID());
+            final LoginException e = new LoginException(AuthnEventIds.NO_CREDENTIALS);
+            if (errorHandler != null) { 
+                errorHandler.handleError(profileRequestContext, authenticationContext, e,
+                        AuthnEventIds.NO_CREDENTIALS);
+            }
+            throw e;
+        }
+        
+        final String username = usernamePasswordContext.getTransformedUsername();
+        log.debug("{} Attempting to authenticate effective client ID '{}' ", getLogPrefix(), username);
+        
+        final Secret secret = clientInformation.getSecret();
+        if (secret.getValue().startsWith("{SHA2}")) {
+            if (secret.getValue().substring(6).equals(digester.apply(usernamePasswordContext.getPassword()))) {
+                log.info("{} Login by '{}' succeeded", getLogPrefix(), username);
+                return populateSubject(new Subject(), usernamePasswordContext);
+            }
+        } else if (clientInformation.getSecret().getValue().equals(usernamePasswordContext.getPassword())) {
+            log.info("{} Login by '{}' succeeded", getLogPrefix(), username);
+            return populateSubject(new Subject(), usernamePasswordContext);
+        }
+        
+        log.info("{} Login by '{}' failed", getLogPrefix(), username);
+        
+        final LoginException e = new LoginException(AuthnEventIds.INVALID_CREDENTIALS); 
+        if (errorHandler != null) { 
+            errorHandler.handleError(profileRequestContext, authenticationContext, e,
+                    AuthnEventIds.INVALID_CREDENTIALS);
+        }
+        throw e;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/OIDCClienInfoCredentialValidatorTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/OIDCClienInfoCredentialValidatorTest.java
new file mode 100644
index 00000000..44b9b786
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/OIDCClienInfoCredentialValidatorTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.oidc.op.authn.impl;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.security.auth.login.LoginException;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.AuthenticationErrorContext;
+import net.shibboleth.idp.authn.context.UsernamePasswordContext;
+import net.shibboleth.idp.authn.impl.ValidateCredentials;
+import net.shibboleth.idp.authn.impl.testing.BaseAuthenticationContextTest;
+import net.shibboleth.idp.authn.principal.UsernamePrincipal;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.utilities.java.support.codec.StringDigester;
+import net.shibboleth.utilities.java.support.codec.StringDigester.OutputFormat;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+/** Unit test for {@link OIDCClienInfoCredentialValidator}. */
+public class OIDCClienInfoCredentialValidatorTest extends BaseAuthenticationContextTest {
+
+    private ClientID clientId;
+    private Secret clientSecret;
+    
+    private OIDCClienInfoCredentialValidator validator;
+    
+    private ValidateCredentials action;
+
+    @BeforeMethod public void setUp() throws ComponentInitializationException {
+        super.setUp();
+
+        clientId = new ClientID("mockId");
+        clientSecret = new Secret("secret1234567890secret1234567890secret1234567890");
+        
+        validator = new OIDCClienInfoCredentialValidator();
+        validator.setId("test");
+        validator.initialize();
+        
+        action = new ValidateCredentials();
+        action.setValidators(Collections.singletonList(validator));
+        
+        final Map<String,Collection<String>> mappings = new HashMap<>();
+        mappings.put("InvalidPassword", Collections.singleton(AuthnEventIds.INVALID_CREDENTIALS));
+        mappings.put(AuthnEventIds.UNKNOWN_USERNAME, Collections.singleton(AuthnEventIds.UNKNOWN_USERNAME));
+        action.setClassifiedMessages(mappings);
+
+        action.initialize();
+
+        final OIDCMetadataContext oidcContext = new OIDCMetadataContext();
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        final Secret secret = new Secret("secret1234567890secret1234567890secret1234567890");
+        final OIDCClientInformation clientInformation = 
+                new OIDCClientInformation(clientId, new Date(), metadata, secret);
+        oidcContext.setClientInformation(clientInformation);
+        prc.getInboundMessageContext().addSubcontext(oidcContext);
+    }
+
+    @Test public void testMissingFlow() {
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_AUTHN_CTX);
+    }
+
+    @Test public void testMissingUser() {
+        prc.getSubcontext(AuthenticationContext.class).setAttemptedFlow(authenticationFlows.get(0));
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testMissingUser2() {
+        final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.getSubcontext(UsernamePasswordContext.class, true);
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testBadPassword() {
+        final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.getSubcontext(UsernamePasswordContext.class, true).setUsername(clientId.getValue()).setPassword("foo");
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, "InvalidPassword");
+        final AuthenticationErrorContext errorCtx = ac.getSubcontext(AuthenticationErrorContext.class);
+        Assert.assertTrue(errorCtx.getExceptions().get(0) instanceof LoginException);
+        Assert.assertFalse(errorCtx.isClassifiedError("UnknownUsername"));
+        Assert.assertTrue(errorCtx.isClassifiedError("InvalidPassword"));
+    }
+
+    @Test public void testAuthorized() {
+        final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.getSubcontext(UsernamePasswordContext.class, true).setUsername(clientId.getValue()).setPassword(clientSecret.getValue());
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        Assert.assertNotNull(ac.getAuthenticationResult());
+        Assert.assertEquals(ac.getAuthenticationResult().getSubject().getPrincipals(UsernamePrincipal.class).iterator()
+                .next().getName(), clientId.getValue());
+    }
+    
+    @Test public void testAuthorizedSHA2() throws NoSuchAlgorithmException {
+        final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.getSubcontext(UsernamePasswordContext.class, true).setUsername(clientId.getValue()).setPassword(clientSecret.getValue());
+
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        final Secret secret = new Secret("{SHA2}" + new StringDigester("SHA-256", OutputFormat.BASE64).apply(clientSecret.getValue()));
+        final OIDCClientInformation clientInformation = 
+                new OIDCClientInformation(clientId, new Date(), metadata, secret);
+        prc.getInboundMessageContext().getSubcontext(OIDCMetadataContext.class).setClientInformation(clientInformation);
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        Assert.assertNotNull(ac.getAuthenticationResult());
+        Assert.assertEquals(ac.getAuthenticationResult().getSubject().getPrincipals(UsernamePrincipal.class).iterator()
+                .next().getName(), clientId.getValue());
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/ValidateClientAuthenticationTypeTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/ValidateClientAuthenticationTypeTest.java
index 7117eb7d..d77d5968 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/ValidateClientAuthenticationTypeTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/ValidateClientAuthenticationTypeTest.java
@@ -17,13 +17,6 @@
 
 package net.shibboleth.idp.plugin.oidc.op.authn.impl;
 
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.NoSuchAlgorithmException;
-import java.security.interfaces.RSAPrivateKey;
-import java.security.interfaces.RSAPublicKey;
 import java.util.Collections;
 import java.util.Date;
 import java.util.Set;
@@ -32,18 +25,14 @@ import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.springframework.webflow.execution.Event;
 import org.springframework.webflow.execution.RequestContext;
-import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import com.nimbusds.jose.JOSEException;
-import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
 import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
 import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
-import com.nimbusds.oauth2.sdk.auth.ClientSecretJWT;
 import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
-import com.nimbusds.oauth2.sdk.auth.PrivateKeyJWT;
 import com.nimbusds.oauth2.sdk.auth.Secret;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
@@ -66,11 +55,6 @@ public class ValidateClientAuthenticationTypeTest {
     private ClientID clientId;
     private Secret clientSecret;
     
-    private URI endpointUri;
-    
-    private RSAPrivateKey rsaPrivateKey;
-    private RSAPublicKey rsaPublicKey;
-    
     private ValidateClientAuthenticationType action;
     
     private RequestContext rc;
@@ -78,20 +62,10 @@ public class ValidateClientAuthenticationTypeTest {
     
     private Set<ClientAuthenticationMethod> enabledMethods;
     
-    @BeforeClass
-    public void initKeys() throws NoSuchAlgorithmException {
-        final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
-        keyGen.initialize(2048);
-        final KeyPair keyPair = keyGen.genKeyPair();
-        rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
-        rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
-    }
-
     @BeforeMethod
-    public void init() throws URISyntaxException, ComponentInitializationException {
+    public void init() throws ComponentInitializationException {
         clientId = new ClientID("mockId");
         clientSecret = new Secret("secret1234567890secret1234567890secret1234567890");
-        endpointUri = new URI("https://mock.example.org/");
         enabledMethods = Collections.emptySet();
         
         action = new ValidateClientAuthenticationType();
@@ -109,10 +83,6 @@ public class ValidateClientAuthenticationTypeTest {
             clientAuth = new ClientSecretBasic(clientId, clientSecret);
         } else if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
             clientAuth = new ClientSecretPost(clientId, clientSecret);
-        } else if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
-            clientAuth = new ClientSecretJWT(clientId, endpointUri, JWSAlgorithm.HS256, clientSecret);
-        } else if (method.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
-            clientAuth = new PrivateKeyJWT(clientId, endpointUri, JWSAlgorithm.RS256, rsaPrivateKey, null, null);
         } else {
             clientAuth = null;
         }

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list