[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 21 22:14:30 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=b2984c21a1071c50c5781b96fc6a3089fae7e095
The following commit(s) were added to refs/heads/main by this push:
new b2984c21 JOIDC-64 - Refactor client authn on OAuth2 endpoints into a login flow
b2984c21 is described below
commit b2984c21a1071c50c5781b96fc6a3089fae7e095
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Dec 21 17:14:27 2021 -0500
JOIDC-64 - Refactor client authn on OAuth2 endpoints into a login flow
https://shibboleth.atlassian.net/browse/JOIDC-64
Implement JWT validator (subject to existing bugs).
---
.../oidc/op/authn/impl/JWTCredentialValidator.java | 204 ++++++++++++++++
.../impl/ValidateEndpointAuthentication.java | 243 -------------------
.../authn/OAuth2Client/OAuth2Client-beans.xml | 3 +-
.../op/authn/impl/JWTCredentialValidatorTest.java | 219 +++++++++++++++++
.../plugin/oidc/op/profile/flow/TokenFlowTest.java | 4 +-
.../impl/ValidateEndpointAuthenticationTest.java | 258 ---------------------
6 files changed, 427 insertions(+), 504 deletions(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidator.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidator.java
new file mode 100644
index 00000000..dbfc8aec
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidator.java
@@ -0,0 +1,204 @@
+/*
+ * 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.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.AbstractCredentialValidator;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.principal.UsernamePrincipal;
+import net.shibboleth.idp.plugin.oidc.op.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.oidc.security.impl.JWTSignatureValidationUtil;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
+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.xmlsec.context.SecurityParametersContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.auth.JWTAuthentication;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+/**
+ * A validator that handles authentication via signed JWT.
+ *
+ * <p>For now, implemented via Nimbus APIs.</p>
+ *
+ * TODO: there will be additional validation checks added once implemented on the older branch
+ */
+ at ThreadSafeAfterInit
+public class JWTCredentialValidator extends AbstractCredentialValidator {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(JWTCredentialValidator.class);
+
+ /** Strategy that will return {@link OAuth2ClientAuthenticationContext}. */
+ @Nonnull private Function<ProfileRequestContext,OAuth2ClientAuthenticationContext> clientAuthContextLookupStrategy;
+
+ /** Strategy used to locate the {@link SecurityParametersContext} to use for verification. */
+ @Nonnull private Function<ProfileRequestContext,SecurityParametersContext> securityParametersLookupStrategy;
+
+ /** Whether to save the JWT in the Java Subject's public credentials. */
+ private boolean saveTokenToCredentialSet;
+
+ /** Constructor. */
+ public JWTCredentialValidator() {
+ // PRC -> AuthenticationContext -> OAuth2ClientAuthenticationContext
+ clientAuthContextLookupStrategy = new ChildContextLookup<>(OAuth2ClientAuthenticationContext.class).compose(
+ new ChildContextLookup<>(AuthenticationContext.class));
+ // PRC -> RP -> SPC
+ securityParametersLookupStrategy = new ChildContextLookup<>(SecurityParametersContext.class).compose(
+ new ChildContextLookup<>(RelyingPartyContext.class));
+ }
+
+ /**
+ * Set the strategy used to return the {@link OAuth2ClientAuthenticationContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setOAuth2ClientAuthenticationLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,OAuth2ClientAuthenticationContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ clientAuthContextLookupStrategy =
+ Constraint.isNotNull(strategy, "OAuth2ClientAuthenticationContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the {@link SecurityParametersContext} to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setSecurityParametersLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,SecurityParametersContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ securityParametersLookupStrategy =
+ Constraint.isNotNull(strategy, "SecurityParameterContext lookup strategy cannot be null");
+ }
+
+
+ /**
+ * Set whether to save the JWT in the Java Subject's public credentials.
+ *
+ * <p>Defaults to true</p>
+ *
+ * @param flag flag to set
+ */
+ public void setSaveTokenToCredentialSet(final boolean flag) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ saveTokenToCredentialSet = flag;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable protected Subject doValidate(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @Nullable final WarningHandler warningHandler,
+ @Nullable final ErrorHandler errorHandler) throws Exception {
+
+ final OAuth2ClientAuthenticationContext clientAuthContext =
+ clientAuthContextLookupStrategy.apply(profileRequestContext);
+ if (clientAuthContext == null || clientAuthContext.getClientAuthentication() == null) {
+ log.debug("{} No OAuth 2.0 client authentication information found", getLogPrefix());
+ return null;
+ }
+
+ final ClientAuthentication clientAuth = clientAuthContext.getClientAuthentication();
+ if (!ClientAuthenticationMethod.CLIENT_SECRET_JWT.equals(clientAuth.getMethod()) &&
+ !ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(clientAuth.getMethod())) {
+ log.debug("{} OAuth client authentication for '{}' of unsupported type: {}", getLogPrefix(),
+ clientAuth.getClientID(), clientAuth.getMethod());
+ return null;
+ }
+
+ if (!(clientAuth instanceof JWTAuthentication)) {
+ log.warn("{} OAuth client authentication object of unexpected type: {}", getLogPrefix(),
+ clientAuth.getClass().getSimpleName());
+ log.info("{} Login by '{}' failed", getLogPrefix(), clientAuth.getClientID());
+ final LoginException e = new LoginException(AuthnEventIds.INVALID_CREDENTIALS);
+ if (errorHandler != null) {
+ errorHandler.handleError(profileRequestContext, authenticationContext, e,
+ AuthnEventIds.INVALID_CREDENTIALS);
+ }
+ throw e;
+ }
+
+ final JWTAuthentication jwtAuth = (JWTAuthentication) clientAuth;
+ final String errorEventId = JWTSignatureValidationUtil.validateSignature(
+ securityParametersLookupStrategy.apply(profileRequestContext), jwtAuth.getClientAssertion(),
+ AuthnEventIds.INVALID_CREDENTIALS);
+ if (errorEventId != null) {
+ log.info("{} Login by '{}' failed", getLogPrefix(), clientAuth.getClientID());
+ final LoginException e = new LoginException(errorEventId);
+ if (errorHandler != null) {
+ errorHandler.handleError(profileRequestContext, authenticationContext, e,
+ AuthnEventIds.INVALID_CREDENTIALS);
+ }
+ throw e;
+ }
+
+ log.info("{} Login by '{}' succeeded", getLogPrefix(), clientAuth.getClientID());
+
+ return populateSubject(clientAuth.getClientID(), jwtAuth.getClientAssertion());
+ }
+
+ /**
+ * Builds a subject with "standard" content from the validation.
+ *
+ * @param clientId client ID
+ * @param token the token validated
+ *
+ * @return the decorated subject
+ */
+ @Nonnull protected Subject populateSubject(@Nonnull @NotEmpty final ClientID clientId,
+ @Nonnull final SignedJWT token) {
+
+ final Subject subject = new Subject();
+ subject.getPrincipals().add(new UsernamePrincipal(clientId.getValue()));
+ if (saveTokenToCredentialSet) {
+ subject.getPublicCredentials().add(token);
+ }
+
+ return super.populateSubject(subject);
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateEndpointAuthentication.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateEndpointAuthentication.java
deleted file mode 100644
index cba47e49..00000000
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateEndpointAuthentication.java
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- * 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.profile.impl;
-
-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.opensaml.xmlsec.context.SecurityParametersContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.nimbusds.jwt.SignedJWT;
-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.JWTAuthentication;
-import com.nimbusds.oauth2.sdk.auth.PlainClientSecret;
-import com.nimbusds.oauth2.sdk.auth.Secret;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
-
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
-import net.shibboleth.oidc.profile.config.navigate.TokenEndpointAuthMethodLookupFunction;
-import net.shibboleth.oidc.security.impl.JWTSignatureValidationUtil;
-import net.shibboleth.oidc.security.impl.OIDCSignatureValidationParameters;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
-/**
- * Validates the endpoint authentication with the token_endpoint_auth_method stored to the client's metadata.
- */
-public class ValidateEndpointAuthentication extends AbstractOIDCRequestAction<AbstractOptionallyAuthenticatedRequest> {
-
- /** Class logger. */
- @Nonnull private Logger log = LoggerFactory.getLogger(ValidateEndpointAuthentication.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 signature validation parameters. */
- @Nullable private OIDCSignatureValidationParameters signatureValidationParameters;
-
- /**
- * Strategy used to locate the {@link SecurityParametersContext} to use for signing.
- */
- @Nonnull private Function<ProfileRequestContext, SecurityParametersContext> securityParametersLookupStrategy;
-
-
- /**
- * Constructor.
- */
- public ValidateEndpointAuthentication() {
- oidcMetadataContextLookupStrategy = new ChildContextLookup<>(OIDCMetadataContext.class).compose(
- new InboundMessageContextLookup());
- tokenEndpointAuthMethodsLookupStrategy = new TokenEndpointAuthMethodLookupFunction();
- securityParametersLookupStrategy = new ChildContextLookup<>(SecurityParametersContext.class);
- }
-
- /**
- * 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");
-
- }
-
- /**
- * Set the strategy used to locate the {@link SecurityParametersContext} to use.
- *
- * @param strategy lookup strategy
- */
- public void setSecurityParametersLookupStrategy(
- @Nonnull final Function<ProfileRequestContext, SecurityParametersContext> strategy) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
- securityParametersLookupStrategy =
- Constraint.isNotNull(strategy, "SecurityParameterContext lookup strategy cannot be null");
- }
-
- /** {@inheritDoc} */
- @Override
- protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- if (!super.doPreExecute(profileRequestContext)) {
- return false;
- }
-
- oidcMetadataContext = oidcMetadataContextLookupStrategy.apply(profileRequestContext);
- if (oidcMetadataContext == null) {
- log.error("{} OICDMetadataContext is null", getLogPrefix());
- // TODO: should this be an error signal?
- return false;
- }
- return true;
- }
-
- // Checkstyle: CyclomaticComplexity OFF
-
- /** {@inheritDoc} */
- @Override
- protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- final AbstractOptionallyAuthenticatedRequest request = getRequest();
- final OIDCClientInformation clientInformation = oidcMetadataContext.getClientInformation();
- final OIDCClientMetadata clientMetadata = clientInformation.getOIDCMetadata();
- final ClientAuthenticationMethod clientAuthMethod = clientMetadata.getTokenEndpointAuthMethod() != null ?
- clientMetadata.getTokenEndpointAuthMethod() : ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
- final ClientAuthentication clientAuth = request.getClientAuthentication();
- final Set<ClientAuthenticationMethod> enabledMethods =
- tokenEndpointAuthMethodsLookupStrategy.apply(profileRequestContext);
-
- if (enabledAndEquals(enabledMethods, clientAuthMethod, ClientAuthenticationMethod.NONE)) {
- log.debug("{} None authentication is requested and enabled, nothing to do", getLogPrefix());
- return;
- } else if (enabledAndEquals(enabledMethods, clientAuthMethod,
- ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
- if (clientAuth instanceof ClientSecretBasic) {
- if (validateSecret((ClientSecretBasic)clientAuth, clientInformation)) {
- return;
- }
- } else {
- log.warn("{} Unrecognized client authentication {} for client_secret_basic", getLogPrefix(),
- request.getClientAuthentication());
- }
- } else if (enabledAndEquals(enabledMethods, clientAuthMethod, ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
- if (clientAuth instanceof ClientSecretPost) {
- if (validateSecret((ClientSecretPost)clientAuth, clientInformation)) {
- return;
- }
- } else {
- log.warn("{} Unrecognized client authentication {} for client_secret_post", getLogPrefix(),
- request.getClientAuthentication());
- }
- } else if (enabledAndEquals(enabledMethods, clientAuthMethod, ClientAuthenticationMethod.CLIENT_SECRET_JWT)
- || enabledAndEquals(enabledMethods, clientAuthMethod, ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
- if (clientAuth instanceof JWTAuthentication) {
- final SignedJWT jwt = ((JWTAuthentication) clientAuth).getClientAssertion();
- final String errorEventId = JWTSignatureValidationUtil.validateSignature(
- securityParametersLookupStrategy.apply(profileRequestContext), jwt, EventIds.ACCESS_DENIED);
- if (errorEventId != null) {
- ActionSupport.buildEvent(profileRequestContext, errorEventId);
- }
- return;
- }
- } else {
- log.warn("{} Unsupported client authentication method {}", getLogPrefix(), clientAuth.getMethod());
- }
- ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
- }
-
- // Checkstyle: CyclomaticComplexity ON
-
- /**
- * Checks whether the requested authentication method is enabled and matching to the desired method.
- * @param enabledMethods The list of enabled authentication method.
- * @param requestedMethod The requested authentication method to be checked.
- * @param desiredMethod The desired authentication method.
- * @return True if enabled and matching, false otherwise.
- */
- protected boolean enabledAndEquals(final Set<ClientAuthenticationMethod> enabledMethods,
- final ClientAuthenticationMethod requestedMethod, final ClientAuthenticationMethod desiredMethod) {
- if (requestedMethod.equals(desiredMethod)) {
- if (enabledMethods == null || enabledMethods.isEmpty()) {
- log.warn("{} List of enabled methods is empty, all methods are disabled", getLogPrefix());
- return false;
- }
- if (!enabledMethods.contains(requestedMethod)) {
- log.warn("{} The requested method {} is not enabled", getLogPrefix(), requestedMethod);
- return false;
- }
- return true;
- }
- return false;
- }
-
- /**
- * Validates the given client secret against the one stored in the client's metadata.
- * @param secret The secret to be validated.
- * @param clientInformation The client metadata.
- * @return True if the secret was valid, false otherwise.
- */
- protected boolean validateSecret(final PlainClientSecret secret, final OIDCClientInformation clientInformation) {
- final Secret clientSecret = secret.getClientSecret();
- if (clientSecret == null) {
- log.warn("{} The client secret was null and cannot be validated", getLogPrefix());
- return false;
- }
- //TODO: should support other than plaintext storage
- if (clientSecret.equals(clientInformation.getSecret())) {
- log.debug("{} The client secret successfully verified", getLogPrefix());
- return true;
- }
- log.warn("{} The client secret validation failed", getLogPrefix());
- return false;
- }
-
-}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OAuth2Client/OAuth2Client-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OAuth2Client/OAuth2Client-beans.xml
index e3c66b11..3ebbed75 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OAuth2Client/OAuth2Client-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OAuth2Client/OAuth2Client-beans.xml
@@ -59,7 +59,8 @@
<util:list id="DefaultOAuth2ClientValidators">
<bean class="net.shibboleth.idp.plugin.oidc.op.authn.impl.OIDCClientInfoCredentialValidator"
p:id="oauth2-clientinfo" />
- <!-- TODO: Add JWT Validators once implemented -->
+ <bean class="net.shibboleth.idp.plugin.oidc.op.authn.impl.JWTCredentialValidator"
+ p:id="oauth2-jwt" />
</util:list>
<!-- Validator parent beans -->
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidatorTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidatorTest.java
new file mode 100644
index 00000000..6a6416fa
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidatorTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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 javax.crypto.spec.SecretKeySpec;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.xmlsec.context.SecurityParametersContext;
+import org.springframework.webflow.execution.Event;
+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.jose.jwk.RSAKey;
+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.ClientSecretJWT;
+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.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.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.oidc.security.credential.BasicJWKCredential;
+import net.shibboleth.oidc.security.impl.OIDCSignatureValidationParameters;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Unit tests for {@link JWTCredentialValidator}.
+ */
+public class JWTCredentialValidatorTest extends BaseAuthenticationContextTest {
+
+ ClientID clientId;
+ Secret clientSecret;
+
+ URI endpointUri;
+
+ RSAPrivateKey rsaPrivateKey;
+ RSAPublicKey rsaPublicKey;
+
+ private JWTCredentialValidator validator;
+ private ValidateCredentials action;
+
+ @BeforeClass
+ public void initKeys() throws NoSuchAlgorithmException {
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048);
+ final KeyPair keyPair = keyGen.genKeyPair();
+ rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
+ rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
+ }
+
+ @BeforeMethod
+ public void setUo() throws URISyntaxException, ComponentInitializationException {
+ super.setUp();
+
+ clientId = new ClientID("mockId");
+ clientSecret = new Secret("secret1234567890secret1234567890secret1234567890");
+ endpointUri = new URI("https://mock.example.org/");
+
+ validator = new JWTCredentialValidator();
+ validator.setId("test");
+ validator.setSecurityParametersLookupStrategy(new ChildContextLookup<>(SecurityParametersContext.class));
+ validator.initialize();
+
+ action = new ValidateCredentials();
+ action.setValidators(Collections.singletonList(validator));
+ action.initialize();
+ }
+
+ protected void completeSetup(final TokenRequest request, final ClientAuthenticationMethod storedMethod,
+ final boolean sameSecret) throws NoSuchAlgorithmException, JOSEException {
+
+ final OIDCMetadataContext oidcContext = new OIDCMetadataContext();
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setTokenEndpointAuthMethod(storedMethod);
+
+ BasicJWKCredential credential = null;
+ final OIDCSignatureValidationParameters params = new OIDCSignatureValidationParameters();
+ if (storedMethod.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
+ params.setSignatureAlgorithm("RS256");
+ credential = new BasicJWKCredential();
+ credential.setAlgorithm(JWSAlgorithm.parse("RS256"));
+ final RSAKey rsaKey;
+ if (sameSecret) {
+ rsaKey = new RSAKey.Builder(rsaPublicKey).build();
+ } else {
+ final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(1024);
+ rsaKey = new RSAKey.Builder((RSAPublicKey)keyGen.genKeyPair().getPublic()).build();
+ }
+ credential.setPublicKey(rsaKey.toPublicKey());
+ } else if (storedMethod.equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
+ params.setSignatureAlgorithm("HS256");
+ credential = new BasicJWKCredential();
+ credential.setAlgorithm(JWSAlgorithm.parse("HS256"));
+ if (sameSecret) {
+ credential.setSecretKey(new SecretKeySpec(clientSecret.getValueBytes(), "NONE"));
+ } else {
+ credential.setSecretKey(new SecretKeySpec("secret1234567890secret1234567890secretWRONG".getBytes(), "NONE"));
+ }
+ }
+
+ if (credential != null) {
+ final SecurityParametersContext secCtx =
+ (SecurityParametersContext) prc.addSubcontext(new SecurityParametersContext());
+ params.getValidationCredentials().add(credential);
+ secCtx.setSignatureSigningParameters(params);
+ }
+
+ final OIDCClientInformation clientInformation =
+ new OIDCClientInformation(clientId, new Date(), metadata, clientSecret);
+ oidcContext.setClientInformation(clientInformation);
+ prc.getInboundMessageContext().addSubcontext(oidcContext);
+ }
+
+ protected void initializeTokenRequest(final ClientAuthenticationMethod method, final boolean success)
+ throws JOSEException, NoSuchAlgorithmException {
+
+ final ClientAuthentication clientAuth;
+ 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 AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class, true);
+ ac.setAttemptedFlow(authenticationFlows.get(0));
+ ac.getSubcontext(OAuth2ClientAuthenticationContext.class, true).setClientAuthentication(clientAuth);
+
+ final AuthorizationGrant authzGrant = new AuthorizationCodeGrant(new AuthorizationCode(), null);
+ completeSetup(new TokenRequest(null, clientAuth, authzGrant), method, success);
+ }
+
+ @Test
+ public void testSecretJwt() throws JOSEException, NoSuchAlgorithmException {
+ initializeTokenRequest(ClientAuthenticationMethod.CLIENT_SECRET_JWT, true);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+
+ final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+ Assert.assertNotNull(ac.getAuthenticationResult());
+ Assert.assertEquals(ac.getAuthenticationResult().getSubject().getPrincipals(UsernamePrincipal.class).iterator()
+ .next().getName(), clientId.getValue());
+ }
+
+ @Test
+ public void testPrivateKeyJwt() throws JOSEException, NoSuchAlgorithmException {
+ initializeTokenRequest(ClientAuthenticationMethod.PRIVATE_KEY_JWT, true);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+
+ final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+ Assert.assertNotNull(ac.getAuthenticationResult());
+ Assert.assertEquals(ac.getAuthenticationResult().getSubject().getPrincipals(UsernamePrincipal.class).iterator()
+ .next().getName(), clientId.getValue());
+ }
+
+ @Test
+ public void testFailingSecretJwt() throws JOSEException, NoSuchAlgorithmException {
+ initializeTokenRequest(ClientAuthenticationMethod.CLIENT_SECRET_JWT, false);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_CREDENTIALS);
+ }
+
+ @Test
+ public void testFailingPrivateKeyJwt() throws JOSEException, NoSuchAlgorithmException {
+ initializeTokenRequest(ClientAuthenticationMethod.PRIVATE_KEY_JWT, false);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_CREDENTIALS);
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
index bc7cc61e..3e953e2f 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
@@ -239,7 +239,7 @@ public class TokenFlowTest extends AbstractOidcFlowTest {
Instant.now().plusSeconds(30))).getValue();
}
- @Test(enabled=false)
+ @Test
public void testValidSecretJWT() throws ParseException, IOException, NoSuchAlgorithmException, URISyntaxException,
DataSealerException, ComponentInitializationException, JOSEException {
ClientSecretJWT clientAuth = buildSecretJwtAuth(clientSecret);
@@ -248,7 +248,7 @@ public class TokenFlowTest extends AbstractOidcFlowTest {
Assert.assertNotNull(response.getTokens().getAccessToken());
}
- @Test(enabled=false)
+ @Test
public void testValidSecretJWTNoAlg() throws ParseException, IOException, NoSuchAlgorithmException,
URISyntaxException, DataSealerException, ComponentInitializationException, JOSEException {
ClientSecretJWT clientAuth = buildSecretJwtAuth(clientSecret);
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateEndpointAuthenticationTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateEndpointAuthenticationTest.java
deleted file mode 100644
index 4b3b267b..00000000
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateEndpointAuthenticationTest.java
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- * 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.profile.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.Date;
-import java.util.Set;
-import java.util.function.Function;
-
-import javax.crypto.spec.SecretKeySpec;
-
-import org.opensaml.profile.action.EventIds;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.xmlsec.context.SecurityParametersContext;
-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.jose.jwk.RSAKey;
-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 com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
-import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
-
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
-import net.shibboleth.idp.profile.context.navigate.AbstractRelyingPartyLookupFunction;
-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.oidc.security.credential.BasicJWKCredential;
-import net.shibboleth.oidc.security.impl.OIDCSignatureValidationParameters;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-
-/**
- * Unit tests for {@link ValidateEndpointAuthentication}.
- */
-public class ValidateEndpointAuthenticationTest {
-
- ClientID clientId;
- Secret clientSecret;
-
- URI endpointUri;
-
- RSAPrivateKey rsaPrivateKey;
- RSAPublicKey rsaPublicKey;
-
- @BeforeClass
- public void initKeys() throws NoSuchAlgorithmException {
- 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 {
- clientId = new ClientID("mockId");
- clientSecret = new Secret("secret1234567890secret1234567890secret1234567890");
- endpointUri = new URI("https://mock.example.org/");
- }
-
- protected RequestContext initializeRequestCtx(final TokenRequest request,
- final ClientAuthenticationMethod storedMethod, boolean sameSecret) throws Exception {
- final RequestContext requestCtx = new RequestContextBuilder().setInboundMessage(request).buildRequestContext();
-
- final ProfileRequestContext prc = new WebflowRequestContextProfileRequestContextLookup().apply(requestCtx);
-
- final OIDCMetadataContext oidcContext = new OIDCMetadataContext();
- final OIDCClientMetadata metadata = new OIDCClientMetadata();
- metadata.setTokenEndpointAuthMethod(storedMethod);
- final Secret secret = sameSecret ? clientSecret : new Secret("WRONG1234567890secret1234567890secret1234567890");
- if (storedMethod != null) {
- BasicJWKCredential credential = null;
- final OIDCSignatureValidationParameters params = new OIDCSignatureValidationParameters();
-
- if (storedMethod.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
- params.setSignatureAlgorithm("RS256");
- credential = new BasicJWKCredential();
- credential.setAlgorithm(JWSAlgorithm.parse("RS256"));
- final RSAKey rsaKey;
- if (sameSecret) {
- rsaKey = new RSAKey.Builder(rsaPublicKey).build();
- } else {
- final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
- keyGen.initialize(1024);
- rsaKey = new RSAKey.Builder((RSAPublicKey)keyGen.genKeyPair().getPublic()).build();
- }
- credential.setPublicKey(rsaKey.toPublicKey());
- } else if (storedMethod.equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
- params.setSignatureAlgorithm("HS256");
- credential = new BasicJWKCredential();
- credential.setAlgorithm(JWSAlgorithm.parse("HS256"));
- credential.setSecretKey(new SecretKeySpec(secret.getValueBytes(), "NONE"));
- }
-
- if (credential != null) {
- final SecurityParametersContext secCtx =
- (SecurityParametersContext) prc.addSubcontext(new SecurityParametersContext());
- params.getValidationCredentials().add(credential);
- secCtx.setSignatureSigningParameters(params);
- }
- }
- final OIDCClientInformation clientInformation =
- new OIDCClientInformation(clientId, new Date(), metadata, secret);
- oidcContext.setClientInformation(clientInformation);
- prc.getInboundMessageContext().addSubcontext(oidcContext);
- return requestCtx;
- }
-
- protected TokenRequest initializeTokenRequest(final ClientAuthenticationMethod method) throws JOSEException {
- 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;
- }
- AuthorizationGrant authzGrant = new AuthorizationCodeGrant(new AuthorizationCode(), null);
- return new TokenRequest(null, clientAuth, authzGrant);
- }
-
- protected ValidateEndpointAuthentication constructAction(final Function<ProfileRequestContext,
- Set<ClientAuthenticationMethod>> newFunction) throws ComponentInitializationException {
- final ValidateEndpointAuthentication action = new ValidateEndpointAuthentication();
- if (newFunction != null) {
- action.setTokenEndpointAuthMethodsLookupStrategy(newFunction);
- }
- action.initialize();
- return action;
- }
-
- @Test
- public void testNoEnabledMethods() throws Exception {
- final ValidateEndpointAuthentication action = constructAction(null);
- final Event event =
- action.execute(initializeRequestCtx(
- initializeTokenRequest(ClientAuthenticationMethod.CLIENT_SECRET_BASIC), null, true));
- ActionTestingSupport.assertEvent(event, EventIds.ACCESS_DENIED);
- }
-
- protected void testClientAuth(final ClientAuthenticationMethod clientAuth, final boolean success) throws Exception {
- final ValidateEndpointAuthentication action =
- constructAction(new ListMethodsFunction(clientAuth));
- final Event event =
- action.execute(initializeRequestCtx(
- initializeTokenRequest(clientAuth), clientAuth, success));
- if (success) {
- Assert.assertNull(event);
- } else {
- ActionTestingSupport.assertEvent(event, EventIds.ACCESS_DENIED);
- }
- }
-
- protected void testSuccessClientAuth(final ClientAuthenticationMethod clientAuth) throws Exception {
- testClientAuth(clientAuth, true);
- }
-
- protected void testFailingClientAuth(final ClientAuthenticationMethod clientAuth) throws Exception {
- testClientAuth(clientAuth, false);
- }
-
- @Test
- public void testBasic() throws Exception {
- testSuccessClientAuth(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
- }
-
- @Test
- public void testPost() throws Exception {
- testSuccessClientAuth(ClientAuthenticationMethod.CLIENT_SECRET_POST);
- }
-
- @Test
- public void testSecretJwt() throws Exception {
- testSuccessClientAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT);
- }
-
- @Test
- public void testPrivateKeyJwt() throws Exception {
- testSuccessClientAuth(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
- }
-
- @Test
- public void testFailingBasic() throws Exception {
- testFailingClientAuth(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
- }
-
- @Test
- public void testFailingPost() throws Exception {
- testFailingClientAuth(ClientAuthenticationMethod.CLIENT_SECRET_POST);
- }
-
- @Test
- public void testFailingSecretJwt() throws Exception {
- testFailingClientAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT);
- }
-
- @Test
- public void testFailingPrivateKeyJwt() throws Exception {
- testFailingClientAuth(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
- }
-
- class ListMethodsFunction extends AbstractRelyingPartyLookupFunction<Set<ClientAuthenticationMethod>> {
-
- private Set<ClientAuthenticationMethod> set;
-
- public ListMethodsFunction(ClientAuthenticationMethod... methods) {
- set = Set.of(methods);
- }
-
- @Override
- public Set<ClientAuthenticationMethod> apply(ProfileRequestContext input) {
- return set;
- }
- }
-
-}
\ 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