[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