[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
Tue Dec 14 21:51:43 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=cfdb88ffdf11a0c76687dbcaa833fdd55b69e3ce

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

commit cfdb88ffdf11a0c76687dbcaa833fdd55b69e3ce
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Dec 14 16:51:37 2021 -0500

    JOIDC-64 - Refactor client authn on OAuth2 endpoints into a login flow
    
    https://shibboleth.atlassian.net/browse/JOIDC-64
    
    Refactor extraction and method validation steps.
---
 .../context/OAuth2ClientAuthenticationContext.java |  64 +++++++
 .../plugin/oidc/op/authn/context/package-info.java |  22 +++
 .../ExtractClientAuthenticationFromRequest.java    | 142 +++++++++++++++
 .../impl/ValidateClientAuthenticationType.java     | 170 ++++++++++++++++++
 .../plugin/oidc/op/authn/impl/package-info.java    |  22 +++
 ...ExtractClientAuthenticationFromRequestTest.java | 196 +++++++++++++++++++++
 .../impl/ValidateClientAuthenticationTypeTest.java | 181 +++++++++++++++++++
 7 files changed, 797 insertions(+)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/context/OAuth2ClientAuthenticationContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/context/OAuth2ClientAuthenticationContext.java
new file mode 100644
index 00000000..9c7587d8
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/context/OAuth2ClientAuthenticationContext.java
@@ -0,0 +1,64 @@
+/*
+ * 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.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+
+/**
+ * A context containing data about OAuth 2.0 client authentication.
+ * 
+ * <p>Currently implemented using Nimbus APIs.</p> 
+ * 
+ * @parent {@link AuthenticationContext}
+ * @added During an OAuth 2.0 authentication attempt
+ */
+public final class OAuth2ClientAuthenticationContext extends BaseContext {
+    
+    /** Client authentication abstraction. */
+    @Nullable private ClientAuthentication clientAuthentication;
+    
+    /**
+     * Get the OAuth 2 client authentication credentials.
+     * 
+     * @return client authentication credentials
+     */
+    @Nullable public ClientAuthentication getClientAuthentication() {
+        return clientAuthentication;
+    }
+
+    /**
+     * Set the OAuth 2 client authentication credentials.
+     * 
+     * @param creds client authentication credentials
+     * 
+     * @return this context
+     */
+    @Nonnull public OAuth2ClientAuthenticationContext setClientAuthentication(
+            @Nullable final ClientAuthentication creds) {
+        clientAuthentication = creds;
+        return this;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/context/package-info.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/context/package-info.java
new file mode 100644
index 00000000..029f6a85
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/context/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+
+/**
+ * Contexts supporting OIDC/OAuth client authentication.
+ */
+package net.shibboleth.idp.plugin.oidc.op.authn.context;
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/ExtractClientAuthenticationFromRequest.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/ExtractClientAuthenticationFromRequest.java
new file mode 100644
index 00000000..ba846fa5
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/ExtractClientAuthenticationFromRequest.java
@@ -0,0 +1,142 @@
+/*
+ * 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 javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.oauth2.sdk.AbstractOptionallyAuthenticatedRequest;
+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.ClientSecretPost;
+import com.nimbusds.oauth2.sdk.auth.TLSClientAuthentication;
+
+import net.shibboleth.idp.authn.AbstractExtractionAction;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.CertificateContext;
+import net.shibboleth.idp.authn.context.UsernamePasswordContext;
+import net.shibboleth.idp.plugin.oidc.op.authn.context.OAuth2ClientAuthenticationContext;
+
+/**
+ * Extracts OAuth 2 client authentication details from a request and stores them in an
+ * {@link OAuth2ClientAuthenticationContext} beneath the {@link AuthenticationContext} for subsequent
+ * validation.
+ * 
+ * <p>Depending on the form of authentication, additional child contexts may be created to store
+ * extracted credentials, and they may undergo configured transformations. For example, password-based
+ * methods will result in a {@link UsernamePasswordContext}, certificate-based in an {@link CertificateContext},
+ * etc.</p>
+ * 
+ * @pre ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null
+ * @post AuthenticationContext.getSubcontext(OAuth2ClientAuthenticationContext.class) ! null
+ *  along with other contexts as appropriate
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link AuthnEventIds#NO_CREDENTIALS}
+ */
+public class ExtractClientAuthenticationFromRequest extends AbstractExtractionAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ExtractClientAuthenticationFromRequest.class);
+    
+    /** Message to extract credentials from. */
+    @Nullable private AbstractOptionallyAuthenticatedRequest request;
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+            return false;
+        }
+        
+        if (profileRequestContext.getInboundMessageContext() != null) {
+            final Object msg = profileRequestContext.getInboundMessageContext().getMessage();
+            if (msg instanceof AbstractOptionallyAuthenticatedRequest) {
+                request = (AbstractOptionallyAuthenticatedRequest) msg;
+                return true;
+            }
+        }
+        
+        log.warn("{} Inbound message missing or of incorrect type", getLogPrefix());
+        ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+        return false;
+    }
+
+// Checkstyle: CyclomaticComplexity OFF
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+
+        final ClientAuthentication clientAuthentication = request.getClientAuthentication();
+        if (clientAuthentication == null) {
+            log.debug("{} No OAuth client credentials in request", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+        
+        final OAuth2ClientAuthenticationContext ctx =
+                authenticationContext.getSubcontext(OAuth2ClientAuthenticationContext.class, true);
+        ctx.setClientAuthentication(clientAuthentication);
+        
+        // Note the Nimbus APIs appear to prevent the client ID or secret from being null.
+        
+        if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientAuthentication.getMethod())) {
+            final ClientSecretBasic basic = (ClientSecretBasic) clientAuthentication;
+            if (basic.getClientID() != null && basic.getClientSecret() != null) {
+                final UsernamePasswordContext upContext = new UsernamePasswordContext();
+                upContext.setUsername(applyTransforms(basic.getClientID().getValue()))
+                    .setPassword(basic.getClientSecret().getValue());
+                authenticationContext.addSubcontext(upContext, true);
+            } else {
+                log.warn("{} No OAuth client credentials in basic-auth request?", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            }
+        } else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthentication.getMethod())) {
+            final ClientSecretPost post = (ClientSecretPost) clientAuthentication;
+            if (post.getClientID() != null && post.getClientSecret() != null) {
+                final UsernamePasswordContext upContext = new UsernamePasswordContext();
+                upContext.setUsername(applyTransforms(post.getClientID().getValue()))
+                    .setPassword(post.getClientSecret().getValue());
+                authenticationContext.addSubcontext(upContext, true);
+            } else {
+                log.warn("{} No OAuth client credentials in POST request?", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            }
+        } else if (ClientAuthenticationMethod.TLS_CLIENT_AUTH.equals(clientAuthentication.getMethod()) ||
+                ClientAuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH.equals(clientAuthentication.getMethod())) {
+            final TLSClientAuthentication tls = (TLSClientAuthentication) clientAuthentication;
+            if (tls.getClientX509Certificate() != null) {
+                final CertificateContext certContext = new CertificateContext();
+                certContext.setCertificate(tls.getClientX509Certificate());
+                authenticationContext.addSubcontext(certContext, true);
+            }
+        }
+    }
+// Checkstyle: CyclomaticComplexity ON
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/ValidateClientAuthenticationType.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/ValidateClientAuthenticationType.java
new file mode 100644
index 00000000..e7006960
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/ValidateClientAuthenticationType.java
@@ -0,0 +1,170 @@
+/*
+ * 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.util.Collections;
+import java.util.Set;
+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;
+import org.opensaml.profile.action.EventIds;
+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.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.idp.authn.AbstractAuthenticationAction;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.oidc.op.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
+import net.shibboleth.oidc.profile.config.navigate.TokenEndpointAuthMethodLookupFunction;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Validates the client authentication type with the token_endpoint_auth_method stored in the client's metadata
+ * and the profile configuration.
+ * 
+ * @pre {@link OIDCMetadataContext} is available
+ * @pre AuthenticationContext.getSubcontext(OAuth2ClientAuthenticationContext.class) != null
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#ACCESS_DENIED}
+ */
+public class ValidateClientAuthenticationType extends AbstractAuthenticationAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ValidateClientAuthenticationType.class);
+    
+    /** Strategy that will return {@link OIDCMetadataContext}. */
+    @Nonnull private Function<ProfileRequestContext,OIDCMetadataContext> oidcMetadataContextLookupStrategy;
+    
+    /** Strategy to obtain enabled token endpoint authentication methods. */
+    @Nullable private Function<ProfileRequestContext,Set<ClientAuthenticationMethod>> 
+        tokenEndpointAuthMethodsLookupStrategy;
+    
+    /** The attached OIDC metadata context. */
+    @Nullable private OIDCMetadataContext oidcMetadataContext;
+
+    /** The extracted client authentication information. */
+    @Nullable private ClientAuthentication clientAuthentication;
+    
+    /** Enabled client authn methods. */
+    @Nullable @NonnullElements private Set<ClientAuthenticationMethod> enabledMethods;
+    
+    /**
+     * Constructor.
+     */
+    public ValidateClientAuthenticationType() {
+        oidcMetadataContextLookupStrategy = new ChildContextLookup<>(OIDCMetadataContext.class).compose(
+                new InboundMessageContextLookup());
+        tokenEndpointAuthMethodsLookupStrategy = new TokenEndpointAuthMethodLookupFunction();
+    }
+        
+    /**
+     * 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");
+    }
+    
+    /**
+     * Set strategy to obtain enabled token endpoint authentication methods.
+     * @param strategy What to set.
+     */
+    public void setTokenEndpointAuthMethodsLookupStrategy(@Nonnull final Function<ProfileRequestContext, 
+            Set<ClientAuthenticationMethod>> strategy) {
+        tokenEndpointAuthMethodsLookupStrategy = Constraint.isNotNull(strategy, 
+                "Strategy to obtain enabled token endpoint authentication methods cannot be null");
+        
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+            return false;
+        }
+
+        final OAuth2ClientAuthenticationContext oauth2Ctx =
+                authenticationContext.getSubcontext(OAuth2ClientAuthenticationContext.class);
+        if (oauth2Ctx != null) {
+            clientAuthentication = oauth2Ctx.getClientAuthentication();
+        }
+
+        oidcMetadataContext = oidcMetadataContextLookupStrategy.apply(profileRequestContext);
+        if (oidcMetadataContext == null) {
+            log.warn("{} OICDMetadataContext is missing", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
+            return false;
+        }
+        
+        enabledMethods = tokenEndpointAuthMethodsLookupStrategy.apply(profileRequestContext);
+        if (enabledMethods == null) {
+            enabledMethods = Collections.emptySet();
+        }
+        
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+        
+        // Pull the client's registered authn method, or default to client_secret_basic.
+        // The enabledMethods member contains the methods authorized in the configuration as a whole.
+        final OIDCClientInformation clientInformation = oidcMetadataContext.getClientInformation();
+        final OIDCClientMetadata clientMetadata = clientInformation.getOIDCMetadata();
+        final ClientAuthenticationMethod registeredMethod = clientMetadata.getTokenEndpointAuthMethod() != null ? 
+                clientMetadata.getTokenEndpointAuthMethod() : ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
+
+        // Did the client use what it registered and is that still allowed?
+        
+        final ClientAuthenticationMethod used =
+                clientAuthentication != null ? clientAuthentication.getMethod() : ClientAuthenticationMethod.NONE; 
+        if (!registeredMethod.equals(used)) {
+            log.warn("{} Client '{}' registered {} but attempted {}", getLogPrefix(),
+                    clientAuthentication.getClientID(), registeredMethod, used);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
+            return;
+        } else if (!enabledMethods.contains(registeredMethod)) {
+            log.warn("{} Requested method {} is not enabled in profile configuration", getLogPrefix(),
+                    registeredMethod);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
+            return;
+        }
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/package-info.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/package-info.java
new file mode 100644
index 00000000..b742f82b
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+
+/**
+ * Implementation classes supporting OIDC/OAuth client authentication.
+ */
+package net.shibboleth.idp.plugin.oidc.op.authn.impl;
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/ExtractClientAuthenticationFromRequestTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/ExtractClientAuthenticationFromRequestTest.java
new file mode 100644
index 00000000..6c7a9e7e
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/ExtractClientAuthenticationFromRequestTest.java
@@ -0,0 +1,196 @@
+/*
+ * 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.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 org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.Event;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.Assert;
+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.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+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 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.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Unit tests for {@link ExtractClientAuthenticationFromRequest}.
+ */
+public class ExtractClientAuthenticationFromRequestTest {
+    
+    private ClientID clientId;
+    private Secret clientSecret;
+    
+    private URI endpointUri;
+    
+    private RSAPrivateKey rsaPrivateKey;
+    private RSAPublicKey rsaPublicKey;
+    
+    private ExtractClientAuthenticationFromRequest action;
+    
+    private RequestContext rc;
+    private ProfileRequestContext prc;
+    
+    @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 {
+        clientId = new ClientID("mockId");
+        clientSecret = new Secret("secret1234567890secret1234567890secret1234567890");
+        endpointUri = new URI("https://mock.example.org/");
+        
+        action = new ExtractClientAuthenticationFromRequest();
+        action.initialize();
+    }
+    
+    protected void initializeRequestCtx(final ClientAuthenticationMethod method)
+            throws JOSEException, ComponentInitializationException {
+        final ClientAuthentication clientAuth;
+        if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
+            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;
+        }
+        final AuthorizationGrant authzGrant = new AuthorizationCodeGrant(new AuthorizationCode(), null);
+        
+        rc = new RequestContextBuilder()
+                .setInboundMessage(new TokenRequest(null, clientAuth, authzGrant))
+                .buildRequestContext();
+        prc = new WebflowRequestContextProfileRequestContextLookup().apply(rc);
+        prc.addSubcontext(new AuthenticationContext());
+    }
+
+    @Test
+    public void testNoAuthnContext() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        prc.removeSubcontext(AuthenticationContext.class);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertEvent(e, AuthnEventIds.INVALID_AUTHN_CTX);
+    }
+
+    @Test
+    public void testBasic() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertProceedEvent(e);
+        
+        final OAuth2ClientAuthenticationContext oauth =
+                prc.getSubcontext(AuthenticationContext.class).getSubcontext(OAuth2ClientAuthenticationContext.class);
+        Assert.assertNotNull(oauth);
+        Assert.assertEquals(oauth.getClientAuthentication().getClientID(), clientId);
+        Assert.assertEquals(
+                oauth.getClientAuthentication().getMethod(), ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        final UsernamePasswordContext up = oauth.getParent().getSubcontext(UsernamePasswordContext.class);
+        Assert.assertNotNull(up);
+        Assert.assertEquals(up.getUsername(), clientId.getValue());
+        Assert.assertEquals(up.getPassword(), clientSecret.getValue());
+    }
+
+    @Test
+    public void testPost() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.CLIENT_SECRET_POST);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertProceedEvent(e);
+        
+        final OAuth2ClientAuthenticationContext oauth =
+                prc.getSubcontext(AuthenticationContext.class).getSubcontext(OAuth2ClientAuthenticationContext.class);
+        Assert.assertNotNull(oauth);
+        Assert.assertEquals(oauth.getClientAuthentication().getClientID(), clientId);
+        Assert.assertEquals(
+                oauth.getClientAuthentication().getMethod(), ClientAuthenticationMethod.CLIENT_SECRET_POST);
+        final UsernamePasswordContext up = oauth.getParent().getSubcontext(UsernamePasswordContext.class);
+        Assert.assertNotNull(up);
+        Assert.assertEquals(up.getUsername(), clientId.getValue());
+        Assert.assertEquals(up.getPassword(), clientSecret.getValue());
+    }
+
+    @Test
+    public void testSecretJwt() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertProceedEvent(e);
+        
+        final OAuth2ClientAuthenticationContext oauth =
+                prc.getSubcontext(AuthenticationContext.class).getSubcontext(OAuth2ClientAuthenticationContext.class);
+        Assert.assertNotNull(oauth);
+        Assert.assertEquals(oauth.getClientAuthentication().getClientID(), clientId);
+        Assert.assertEquals(
+                oauth.getClientAuthentication().getMethod(), ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+        final UsernamePasswordContext up = oauth.getParent().getSubcontext(UsernamePasswordContext.class);
+        Assert.assertNull(up);
+    }
+
+    @Test
+    public void testPrivateKeyJwt() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertProceedEvent(e);
+
+        final OAuth2ClientAuthenticationContext oauth =
+                prc.getSubcontext(AuthenticationContext.class).getSubcontext(OAuth2ClientAuthenticationContext.class);
+        Assert.assertNotNull(oauth);
+        Assert.assertEquals(oauth.getClientAuthentication().getClientID(), clientId);
+        Assert.assertEquals(
+                oauth.getClientAuthentication().getMethod(), ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+        final UsernamePasswordContext up = oauth.getParent().getSubcontext(UsernamePasswordContext.class);
+        Assert.assertNull(up);
+    }
+
+}
\ 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
new file mode 100644
index 00000000..7117eb7d
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/ValidateClientAuthenticationTypeTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.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;
+
+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;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.oidc.op.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Unit tests for {@link ValidateClientAuthenticationType}.
+ */
+public class ValidateClientAuthenticationTypeTest {
+    
+    private ClientID clientId;
+    private Secret clientSecret;
+    
+    private URI endpointUri;
+    
+    private RSAPrivateKey rsaPrivateKey;
+    private RSAPublicKey rsaPublicKey;
+    
+    private ValidateClientAuthenticationType action;
+    
+    private RequestContext rc;
+    private ProfileRequestContext prc;
+    
+    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 {
+        clientId = new ClientID("mockId");
+        clientSecret = new Secret("secret1234567890secret1234567890secret1234567890");
+        endpointUri = new URI("https://mock.example.org/");
+        enabledMethods = Collections.emptySet();
+        
+        action = new ValidateClientAuthenticationType();
+        action.setTokenEndpointAuthMethodsLookupStrategy(p -> {
+            return enabledMethods;
+            }
+        );
+        action.initialize();
+    }
+    
+    protected void initializeRequestCtx(final ClientAuthenticationMethod method,
+            final ClientAuthenticationMethod storedMethod) throws JOSEException, ComponentInitializationException {
+        final ClientAuthentication clientAuth;
+        if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
+            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;
+        }
+        rc = new RequestContextBuilder().setInboundMessage(null).buildRequestContext();
+        prc = new WebflowRequestContextProfileRequestContextLookup().apply(rc);
+        prc.addSubcontext(new AuthenticationContext()).addSubcontext(
+                new OAuth2ClientAuthenticationContext().setClientAuthentication(clientAuth));
+        
+        final OIDCMetadataContext oidcContext = new OIDCMetadataContext();
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setTokenEndpointAuthMethod(storedMethod);
+        // we're not actually validating the secret so it doesn't matter
+        final Secret secret = new Secret("WRONG1234567890secret1234567890secret1234567890");
+        final OIDCClientInformation clientInformation = 
+                new OIDCClientInformation(clientId, new Date(), metadata, secret);
+        oidcContext.setClientInformation(clientInformation);
+        prc.getInboundMessageContext().addSubcontext(oidcContext);
+    }
+
+    @Test
+    public void testNoAuthnContext() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
+                ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        prc.removeSubcontext(AuthenticationContext.class);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertEvent(e, AuthnEventIds.INVALID_AUTHN_CTX);
+    }
+    
+    @Test
+    public void testNoneDisabled() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.NONE,
+                ClientAuthenticationMethod.NONE);
+        prc.getSubcontext(AuthenticationContext.class).removeSubcontext(OAuth2ClientAuthenticationContext.class);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertEvent(e, EventIds.ACCESS_DENIED);
+    }
+
+    @Test
+    public void testNoneEnabled() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.NONE,
+                ClientAuthenticationMethod.NONE);
+        enabledMethods = Collections.singleton(ClientAuthenticationMethod.NONE);
+        prc.getSubcontext(AuthenticationContext.class).removeSubcontext(OAuth2ClientAuthenticationContext.class);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertProceedEvent(e);
+    }
+    
+    @Test
+    public void testBasicDisabled() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.NONE,
+                ClientAuthenticationMethod.NONE);
+        enabledMethods = Collections.singleton(ClientAuthenticationMethod.CLIENT_SECRET_POST);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertEvent(e, EventIds.ACCESS_DENIED);
+    }
+    
+    @Test
+    public void testBasicEnabled() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
+                ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        enabledMethods = Collections.singleton(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertProceedEvent(e);
+    }
+
+}
\ No newline at end of file

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


More information about the commits mailing list