[java-idp-plugin-totp] branch master updated: Working and tested seed source and validator.

Scott Cantor cantor.2 at osu.edu
Mon Jul 13 21:28:50 UTC 2020


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

scantor pushed a commit to branch master
in repository java-idp-plugin-totp.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-totp.git;a=commit;h=6c7a8207854e6e91662d3120809c8c802ac7a992

The following commit(s) were added to refs/heads/master by this push:
       new  6c7a820   Working and tested seed source and validator.
6c7a820 is described below

commit 6c7a8207854e6e91662d3120809c8c802ac7a992
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Mon Jul 13 17:28:53 2020 -0400

    Working and tested seed source and validator.
---
 pom.xml                                            |   7 +
 totp-impl/.checkstyle                              |   6 +-
 totp-impl/pom.xml                                  |  64 +++++++-
 .../idp/plugin/totp/TOTPDescription.java           |  82 ++++++++++
 .../idp/plugin/totp/context/TOTPContext.java       |  58 -------
 .../totp/impl/AbstractTOTPCredentialValidator.java | 164 +++++++++++++++++++
 .../totp/impl/AttributeResolverSeedSource.java     |   2 +-
 .../GoogleAuthenticatorCredentialValidator.java    | 102 ++++++++++++
 .../shibboleth/idp/plugin/totp/package-info.java   |  22 +++
 .../0.0.1/version.details.properties               |   3 +
 .../versions.properties                            |   1 +
 ...utilities.java.support.plugin.PluginDescription |   1 +
 .../net/shibboleth/idp/plugin/totp/PluginTest.java |  61 +++++++
 .../totp/impl/AttributeResolverSeedSourceTest.java | 156 ++++++++++++++++++
 ...GoogleAuthenticatorCredentialValidatorTest.java | 180 +++++++++++++++++++++
 15 files changed, 843 insertions(+), 66 deletions(-)

diff --git a/pom.xml b/pom.xml
index d1a3386..a28c086 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,6 +56,13 @@
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
+            <dependency>
+                <groupId>${idp.groupId}</groupId>
+                <artifactId>idp-tests-bom</artifactId>
+                <version>${idp.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 
diff --git a/totp-impl/.checkstyle b/totp-impl/.checkstyle
index 7c63424..c9c3e58 100644
--- a/totp-impl/.checkstyle
+++ b/totp-impl/.checkstyle
@@ -1,10 +1,10 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
-<fileset-config file-format-version="1.2.0" simple-config="true" sync-formatter="false">
+<fileset-config file-format-version="1.2.0" simple-config="false" sync-formatter="false">
   <local-check-config name="Shibboleth Checkstyle" location="/java-idp-plugin-totp/checkstyle.xml" type="project" description="">
     <additional-data name="protect-config-file" value="false"/>
   </local-check-config>
-  <fileset name="all" enabled="true" check-config-name="Shibboleth Checkstyle" local="true">
-    <file-match-pattern match-pattern="." include-pattern="true"/>
+  <fileset name="main" enabled="true" check-config-name="Shibboleth Checkstyle" local="true">
+    <file-match-pattern match-pattern="src/main/java/.*\.java$" include-pattern="true"/>
   </fileset>
 </fileset-config>
diff --git a/totp-impl/pom.xml b/totp-impl/pom.xml
index 0a339a4..3c1a9c6 100644
--- a/totp-impl/pom.xml
+++ b/totp-impl/pom.xml
@@ -30,13 +30,13 @@
                 
         <!-- provided dependencies -->
         <dependency>
-            <groupId>net.shibboleth.utilities</groupId>
-            <artifactId>java-support</artifactId>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
             <scope>provided</scope>
         </dependency>
         <dependency>
-            <groupId>net.shibboleth.ext</groupId>
-            <artifactId>spring-extensions</artifactId>
+            <groupId>net.shibboleth.utilities</groupId>
+            <artifactId>java-support</artifactId>
             <scope>provided</scope>
         </dependency>
         <dependency>
@@ -61,6 +61,62 @@
         </dependency>
 
         <!-- test dependencies -->
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-attribute-resolver-api</artifactId>
+            <scope>test</scope>
+            <type>test-jar</type>
+        </dependency>
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-attribute-resolver-impl</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-authn-impl</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-authn-impl</artifactId>
+            <scope>test</scope>
+            <type>test-jar</type>
+        </dependency>
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-installer</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-profile-api</artifactId>
+            <scope>test</scope>
+            <type>test-jar</type>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-core</artifactId>
+            <scope>test</scope>
+            <type>test-jar</type>
+        </dependency>
+        <dependency>
+            <groupId>net.shibboleth.utilities</groupId>
+            <artifactId>java-support</artifactId>
+            <scope>test</scope>
+            <type>test-jar</type>
+        </dependency>
+        <dependency>
+            <groupId>net.shibboleth.ext</groupId>
+            <artifactId>spring-extensions</artifactId>
+            <scope>test</scope>
+            <type>test-jar</type>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-test</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/TOTPDescription.java b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/TOTPDescription.java
new file mode 100644
index 0000000..9de5ea5
--- /dev/null
+++ b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/TOTPDescription.java
@@ -0,0 +1,82 @@
+/*
+ * 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.totp;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import org.springframework.core.io.ClassPathResource;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.collection.Pair;
+import net.shibboleth.utilities.java.support.plugin.AbstractPluginDescription;
+
+/**
+ * Details about the TOTP login plugin.
+ */
+public class TOTPDescription extends AbstractPluginDescription {
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull @NotEmpty public String getPluginId() {
+        return "net.shibboleth.idp.plugin.totp";
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull @NonnullElements public List<URL> getUpdateURLs() throws IOException {
+        return Collections.singletonList(new ClassPathResource("META-INF/plugins/").getURL());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull @NonnullElements public List<Pair<URL, Path>> getExternalFilePathsToCopy() throws IOException {
+        return Collections.emptyList();
+    }
+        
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull @NonnullElements public List<Path> getFilePathsToCopy() {
+        return List.of(Path.of("conf", "testfile.xml"), Path.of("doc", "nashorn-plugin"));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getMajorVersion() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getMinorVersion() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getPatchVersion() {
+        return 1;
+    }
+
+}
\ No newline at end of file
diff --git a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/context/TOTPContext.java b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/context/TOTPContext.java
index 6a151d5..ccc8fd6 100644
--- a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/context/TOTPContext.java
+++ b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/context/TOTPContext.java
@@ -30,32 +30,12 @@ import com.google.common.base.Strings;
 import net.shibboleth.utilities.java.support.annotation.constraint.Live;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
  * Context class for state of a TOTP validation.
  */
 public class TOTPContext extends BaseContext {
 
-    /** Transaction status. */
-    public enum AuthState {
-        /** Success. */
-        OK,
-        /** Failure. */
-        ERROR,
-        /** Unable to locate a token registration. */
-        MISSING_SEED,
-        /** No tokens registered. */
-        REGISTER,
-        /** Unable to validate. */
-        CANT_VALIDATE,
-        /** Unknown. */
-        UNKNOWN,
-    }
-
-    /** State of transaction. */
-    @Nonnull private AuthState state;
-
     /** The subject identifier with respect to the token "back-end". */
     @Nullable @NotEmpty private String username;
 
@@ -70,33 +50,9 @@ public class TOTPContext extends BaseContext {
 
     /** Constructor. */
     public TOTPContext() {
-        state = AuthState.UNKNOWN;
         tokenSeeds = new ArrayList<>();
     }
 
-    /**
-     * Get the state of the transaction.
-     * 
-     * @return transaction state
-     */
-    @Nonnull public AuthState getState() {
-        return state;
-    }
-
-    
-    /**
-     * Set the state of the transaction.
-     * 
-     * @param astate new state
-     * 
-     * @return this context
-     */
-    @Nonnull public TOTPContext setState(@Nonnull final AuthState astate) {
-      state = Constraint.isNotNull(astate, "AuthState cannot be null");
-      
-      return this;
-    }
-
     /**
      * Get the username.
      *
@@ -154,18 +110,4 @@ public class TOTPContext extends BaseContext {
          return tokenSeeds;
      }
 
-     /*
-      //A counter for the numbers of times the user has failed auth in this context
-      private int failedAuth = 0;
-    
-    
-      public void failedAttempt() {
-        failedAuth++;
-      }
-    
-      public int getFailedAttempts(){
-        return failedAuth;
-      }
-    */
-
 }
\ No newline at end of file
diff --git a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AbstractTOTPCredentialValidator.java b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AbstractTOTPCredentialValidator.java
new file mode 100644
index 0000000..e338552
--- /dev/null
+++ b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AbstractTOTPCredentialValidator.java
@@ -0,0 +1,164 @@
+/*
+ * 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.totp.impl;
+
+import java.util.function.Function;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginException;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.authn.AbstractCredentialValidator;
+import net.shibboleth.idp.authn.AbstractUsernamePasswordCredentialValidator;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.CredentialValidator;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.totp.context.TOTPContext;
+import net.shibboleth.idp.plugin.totp.principal.TOTPPrincipal;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * An abstract {@link CredentialValidator} that checks for a {@link TOTPContext} and delegates
+ * to subclasses to produce a result.
+ */
+public abstract class AbstractTOTPCredentialValidator extends AbstractCredentialValidator {
+
+    /** Default prefix for metrics. */
+    @Nonnull @NotEmpty private static final String DEFAULT_METRIC_NAME = "net.shibboleth.idp.authn.totp"; 
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractUsernamePasswordCredentialValidator.class);
+
+    /** Lookup strategy for TOTP context. */
+    @Nonnull private Function<AuthenticationContext,TOTPContext> totpContextLookupStrategy;
+    
+    /** A regular expression to apply for acceptance testing. */
+    @Nullable private Pattern matchExpression;
+    
+    /** Constructor. */
+    public AbstractTOTPCredentialValidator() {
+        totpContextLookupStrategy = new ChildContextLookup<>(TOTPContext.class);
+    }
+    
+    /**
+     * Set the lookup strategy to locate the {@link TOTPContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTOTPContextLookupStrategy(
+            @Nonnull final Function<AuthenticationContext,TOTPContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        totpContextLookupStrategy = Constraint.isNotNull(strategy, "TOTPContextLookupStrategy cannot be null");
+    }
+    
+    /**
+     * Set a matching expression to apply to the username for acceptance. 
+     * 
+     * @param expression a matching expression
+     */
+    public void setMatchExpression(@Nullable final Pattern expression) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        matchExpression = expression;
+    }
+        
+    /** {@inheritDoc} */
+    @Override
+    protected Subject doValidate(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nullable final WarningHandler warningHandler,
+            @Nullable final ErrorHandler errorHandler) throws Exception {
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        
+        final TOTPContext totpContext = totpContextLookupStrategy.apply(authenticationContext);
+        if (totpContext == null) {
+            log.info("{} No TOTPContext available", getLogPrefix());
+            if (errorHandler != null) {
+                errorHandler.handleError(profileRequestContext, authenticationContext, (String) null,
+                        AuthnEventIds.NO_CREDENTIALS);
+            }
+            throw new LoginException(AuthnEventIds.NO_CREDENTIALS);
+        } else if (totpContext.getUsername() == null) {
+            log.info("{} No username available within TOTPContext", getLogPrefix());
+            if (errorHandler != null) {
+                errorHandler.handleError(profileRequestContext, authenticationContext, (String) null,
+                        AuthnEventIds.NO_CREDENTIALS);
+            }
+            throw new LoginException(AuthnEventIds.NO_CREDENTIALS);
+        } else if (totpContext.getTokenCode() == null || totpContext.getTokenSeeds().isEmpty()) {
+            log.info("{} No seeds or tokencode available within TOTPContext", getLogPrefix());
+            if (errorHandler != null) {
+                errorHandler.handleError(profileRequestContext, authenticationContext, (String) null,
+                        AuthnEventIds.INVALID_CREDENTIALS);
+            }
+            throw new LoginException(AuthnEventIds.INVALID_CREDENTIALS);
+        }
+        
+        if (matchExpression != null && !matchExpression.matcher(totpContext.getUsername()).matches()) {
+            log.debug("{} Username '{}' did not match expression", getLogPrefix(), totpContext.getUsername());
+            return null;
+        }
+                
+        return doValidate(profileRequestContext, authenticationContext, totpContext, warningHandler, errorHandler);
+    }
+
+    /**
+     * Override method for subclasses to use to perform the actual TOTP validation.
+     * 
+     * @param profileRequestContext profile request context
+     * @param authenticationContext authentication context
+     * @param totpContext the TOTP context to validate
+     * @param warningHandler optional warning handler interface
+     * @param errorHandler optional error handler interface
+     * 
+     * @return the validated result, or null if inapplicable
+     * 
+     * @throws Exception if an error occurs
+     */
+    @Nullable protected abstract Subject doValidate(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final TOTPContext totpContext,
+            @Nullable final WarningHandler warningHandler,
+            @Nullable final ErrorHandler errorHandler) throws Exception;
+
+    /**
+     * Decorate the subject with "standard" content from the validation.
+     * 
+     * @param subject the subject being returned
+     * @param totpContext the TOTP context being validated
+     * 
+     * @return the decorated subject
+     */
+    @Nonnull protected Subject populateSubject(@Nonnull final Subject subject,
+            @Nonnull final TOTPContext totpContext) {
+        subject.getPrincipals().add(new TOTPPrincipal(totpContext.getUsername()));
+        
+        return super.populateSubject(subject);
+    }
+    
+}
\ No newline at end of file
diff --git a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AttributeResolverSeedSource.java b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AttributeResolverSeedSource.java
index 94ca139..c0f4c2e 100644
--- a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AttributeResolverSeedSource.java
+++ b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AttributeResolverSeedSource.java
@@ -126,7 +126,7 @@ public class AttributeResolverSeedSource extends AbstractSeedSource {
 
             log.debug("Resolved {} seed(s) for '{}'", seeds.size(), totp.getUsername());
         } else {
-            log.warn("Unable to locate TOTPContext and username");
+            log.warn("Unable to locate TOTPContext with username set");
         }
     }
 
diff --git a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/GoogleAuthenticatorCredentialValidator.java b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/GoogleAuthenticatorCredentialValidator.java
new file mode 100644
index 0000000..02651ee
--- /dev/null
+++ b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/GoogleAuthenticatorCredentialValidator.java
@@ -0,0 +1,102 @@
+/*
+ * 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.totp.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginException;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.totp.context.TOTPContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+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.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.warrenstrange.googleauth.GoogleAuthenticator;
+
+/**
+ * A TOTP validator using an implementation based on Google Authenticator code.
+ */
+ at ThreadSafeAfterInit
+public class GoogleAuthenticatorCredentialValidator extends AbstractTOTPCredentialValidator {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(GoogleAuthenticatorCredentialValidator.class);
+    
+    /** Google Authenticator. **/
+    @NonnullAfterInit private GoogleAuthenticator gAuth;
+    
+    /**
+     * Set the {@link GoogleAuthenticator} to use.
+     * 
+     * @param authenticator implementation to use
+     */
+    public void setGoogleAuthenticator(@Nonnull final GoogleAuthenticator authenticator) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        gAuth = Constraint.isNotNull(authenticator, "GoogleAuthenticator cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (gAuth == null) {
+            log.info("{} Installing default GoogleAuthenticator", getLogPrefix());
+            gAuth = new GoogleAuthenticator();
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected Subject doValidate(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final TOTPContext totpContext,
+            @Nullable final WarningHandler warningHandler,
+            @Nullable final ErrorHandler errorHandler) throws Exception {
+        
+        log.debug("{} Attempting to authenticate token code for '{}' ", getLogPrefix(), totpContext.getUsername());
+        
+        try {
+            if (totpContext.getTokenSeeds().stream().anyMatch(
+                    seed -> gAuth.authorize(seed, totpContext.getTokenCode()))) {
+                log.info("{} Login by '{}' succeeded", getLogPrefix(), totpContext.getUsername());
+                return populateSubject(new Subject(), totpContext);
+            }
+            
+            throw new LoginException(AuthnEventIds.INVALID_CREDENTIALS);
+        } catch (final Exception e) {
+            log.info("{} Login by '{}' failed", getLogPrefix(), totpContext.getUsername());
+            if (errorHandler != null) { 
+                errorHandler.handleError(profileRequestContext, authenticationContext, e,
+                        AuthnEventIds.INVALID_CREDENTIALS);
+            }
+            throw e;
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/package-info.java b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/package-info.java
new file mode 100644
index 0000000..6856442
--- /dev/null
+++ b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/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.
+ */
+
+/**
+ * Top level TOTP classes.
+ */
+
+package net.shibboleth.idp.plugin.totp;
\ No newline at end of file
diff --git a/totp-impl/src/main/resources/META-INF/plugins/net.shibboleth.idp.plugin.totp/0.0.1/version.details.properties b/totp-impl/src/main/resources/META-INF/plugins/net.shibboleth.idp.plugin.totp/0.0.1/version.details.properties
new file mode 100644
index 0000000..ccce079
--- /dev/null
+++ b/totp-impl/src/main/resources/META-INF/plugins/net.shibboleth.idp.plugin.totp/0.0.1/version.details.properties
@@ -0,0 +1,3 @@
+idp.version.max=5.0.0
+idp.version.min=4.1.0
+support.level = 2
diff --git a/totp-impl/src/main/resources/META-INF/plugins/net.shibboleth.idp.plugin.totp/versions.properties b/totp-impl/src/main/resources/META-INF/plugins/net.shibboleth.idp.plugin.totp/versions.properties
new file mode 100644
index 0000000..65fbb9d
--- /dev/null
+++ b/totp-impl/src/main/resources/META-INF/plugins/net.shibboleth.idp.plugin.totp/versions.properties
@@ -0,0 +1 @@
+net.shibboleth.idp.plugin.totp.versions=0.0.1
\ No newline at end of file
diff --git a/totp-impl/src/main/resources/META-INF/services/net.shibboleth.utilities.java.support.plugin.PluginDescription b/totp-impl/src/main/resources/META-INF/services/net.shibboleth.utilities.java.support.plugin.PluginDescription
new file mode 100644
index 0000000..ad98521
--- /dev/null
+++ b/totp-impl/src/main/resources/META-INF/services/net.shibboleth.utilities.java.support.plugin.PluginDescription
@@ -0,0 +1 @@
+net.shibboleth.idp.plugin.totp.TOTPDescription
diff --git a/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/PluginTest.java b/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/PluginTest.java
new file mode 100644
index 0000000..1306a4a
--- /dev/null
+++ b/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/PluginTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.totp;
+
+import static org.testng.Assert.assertNotNull;
+
+import java.util.ServiceLoader;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.installer.plugin.impl.PluginState;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.plugin.PluginDescription;
+
+/**  basic sanity tests */
+ at SuppressWarnings("javadoc")
+public class PluginTest {
+
+	private PluginDescription totp;
+
+	@BeforeClass
+	public void SetupPlugin() {
+		final ServiceLoader<PluginDescription> loader = ServiceLoader.load(PluginDescription.class);
+		for (final PluginDescription service:loader) {
+			if (getClass().getPackageName().contentEquals(service.getPluginId())) {
+				totp = service;
+				break;
+			}
+		}
+		assertNotNull(totp);
+	}
+
+	@Test
+	public void testState() throws ComponentInitializationException {
+		
+		final PluginState state = new PluginState(totp); 
+		state.initialize();
+
+	}
+	
+	@Test
+	public void testDownload() throws Exception {
+	    // TODO
+	}
+}
diff --git a/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/impl/AttributeResolverSeedSourceTest.java b/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/impl/AttributeResolverSeedSourceTest.java
new file mode 100644
index 0000000..cf3e1e6
--- /dev/null
+++ b/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/impl/AttributeResolverSeedSourceTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.totp.impl;
+
+import static org.testng.Assert.*;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.ext.spring.service.MockApplicationContext;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.resolver.AttributeDefinition;
+import net.shibboleth.idp.attribute.resolver.DataConnector;
+import net.shibboleth.idp.attribute.resolver.MockAttributeDefinition;
+import net.shibboleth.idp.attribute.resolver.impl.AttributeResolverImpl;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.totp.context.TOTPContext;
+import net.shibboleth.idp.profile.RequestContextBuilder;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.service.MockReloadableService;
+
+/** Test case for {@link AttributeResolverSeedSource}. */
+public class AttributeResolverSeedSourceTest {
+
+    private AttributeResolverSeedSource source;
+    
+    @BeforeMethod
+    public void setUp() throws ComponentInitializationException {
+        final IdPAttribute single = new IdPAttribute("single");
+        single.setValues(Collections.singletonList(StringAttributeValue.valueOf("one")));
+        final AttributeDefinition singledef = new MockAttributeDefinition("single", single);
+        singledef.initialize();
+
+        final IdPAttribute multiple = new IdPAttribute("multiple");
+        multiple.setValues(List.of(StringAttributeValue.valueOf("one"), StringAttributeValue.valueOf(null), StringAttributeValue.valueOf("two")));
+        final AttributeDefinition multipledef = new MockAttributeDefinition("multiple", multiple);
+        multipledef.initialize();
+        
+        final AttributeResolverImpl resolver = newAttributeResolverImpl(List.of(singledef, multipledef), null);
+
+        source = new AttributeResolverSeedSource();
+        source.setAttributeResolver(new MockReloadableService<>(resolver));
+    }
+
+    @Test(expectedExceptions=ComponentInitializationException.class)
+    public void invalid() throws ComponentInitializationException {
+        // No attributeId
+        source.initialize();
+    }
+    
+    @Test public void noContextOrUsername() throws ComponentInitializationException {
+
+        source.setSourceAttribute("foo");
+        source.initialize();
+        
+        // does nothing
+        source.accept(null);
+        
+        final ProfileRequestContext prc = new RequestContextBuilder().buildProfileRequestContext();
+
+        // also does nothing
+        source.accept(prc);
+        
+        final TOTPContext totp = prc.getSubcontext(AuthenticationContext.class, true).getSubcontext(TOTPContext.class, true);
+        source.accept(prc);
+        assertTrue(totp.getTokenSeeds().isEmpty());
+    }
+
+    @Test public void noAttribute() throws ComponentInitializationException {
+
+        source.setSourceAttribute("foo");
+        source.initialize();
+        
+        final ProfileRequestContext prc = new RequestContextBuilder().buildProfileRequestContext();
+        final TOTPContext totp = prc.getSubcontext(AuthenticationContext.class, true).getSubcontext(TOTPContext.class, true);
+        totp.setUsername("jdoe");
+        source.accept(prc);
+        
+        assertTrue(totp.getTokenSeeds().isEmpty());
+    }
+
+    @Test public void single() throws ComponentInitializationException {
+
+        source.setSourceAttribute("single");
+        source.initialize();
+        
+        final ProfileRequestContext prc = new RequestContextBuilder().buildProfileRequestContext();
+        final TOTPContext totp = prc.getSubcontext(AuthenticationContext.class, true).getSubcontext(TOTPContext.class, true);
+        totp.setUsername("jdoe");
+        source.accept(prc);
+        
+        assertEquals(totp.getTokenSeeds().size(), 1);
+        assertEquals(totp.getTokenSeeds().iterator().next(), "one");
+    }
+
+    @Test public void multiple() throws ComponentInitializationException {
+
+        source.setSourceAttribute("multiple");
+        source.initialize();
+        
+        final ProfileRequestContext prc = new RequestContextBuilder().buildProfileRequestContext();
+        final TOTPContext totp = prc.getSubcontext(AuthenticationContext.class, true).getSubcontext(TOTPContext.class, true);
+        totp.setUsername("jdoe");
+        source.accept(prc);
+        
+        assertEquals(totp.getTokenSeeds().size(), 2);
+        assertEquals(totp.getTokenSeeds(), List.of("one", "two"));
+    }
+
+    /**
+     * Mock a resolver.
+     * 
+     * @param definitions
+     * @param connectors
+     * 
+     * @return resolver
+     * @throws ComponentInitializationException 
+     */
+    private static AttributeResolverImpl newAttributeResolverImpl(
+            @Nullable final Collection<AttributeDefinition> definitions,
+            @Nullable final Collection<DataConnector> connectors) throws ComponentInitializationException {
+        final AttributeResolverImpl result = new AttributeResolverImpl();
+        result.setId("test");
+        
+        result.setAttributeDefinitions(definitions == null ? Collections.emptyList() : definitions);
+        result.setDataConnectors(connectors == null ? Collections.emptyList() : connectors);
+        result.setApplicationContext(new MockApplicationContext());
+        result.initialize();
+        
+        return result;
+    }
+
+}
\ No newline at end of file
diff --git a/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/impl/GoogleAuthenticatorCredentialValidatorTest.java b/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/impl/GoogleAuthenticatorCredentialValidatorTest.java
new file mode 100644
index 0000000..fe8b3c1
--- /dev/null
+++ b/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/impl/GoogleAuthenticatorCredentialValidatorTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.totp.impl;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+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.impl.BaseAuthenticationContextTest;
+import net.shibboleth.idp.authn.impl.ValidateCredentials;
+import net.shibboleth.idp.plugin.totp.context.TOTPContext;
+import net.shibboleth.idp.plugin.totp.principal.TOTPPrincipal;
+import net.shibboleth.idp.profile.ActionTestingSupport;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.warrenstrange.googleauth.GoogleAuthenticator;
+import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
+
+/** Unit test for {@link GoogleAuthenticatorCredentialValidator}. */
+public class GoogleAuthenticatorCredentialValidatorTest extends BaseAuthenticationContextTest {
+    
+    private GoogleAuthenticatorCredentialValidator validator;
+    
+    private ValidateCredentials action;
+
+    @BeforeMethod public void setUp() throws Exception {
+        super.setUp();
+
+        validator = new GoogleAuthenticatorCredentialValidator();
+        validator.setId("gauthtest");
+        
+        action = new ValidateCredentials();
+        action.setValidators(Collections.singletonList(validator));
+        
+        final Map<String,Collection<String>> mappings = new HashMap<>();
+        mappings.put(AuthnEventIds.INVALID_CREDENTIALS, Collections.singleton(AuthnEventIds.INVALID_CREDENTIALS));
+        mappings.put(AuthnEventIds.UNKNOWN_USERNAME, Collections.singleton(AuthnEventIds.UNKNOWN_USERNAME));
+        action.setClassifiedMessages(mappings);
+    }
+
+    @Test public void testMissingContext() throws Exception {
+        prc.getSubcontext(AuthenticationContext.class).setAttemptedFlow(authenticationFlows.get(0));
+        
+        validator.initialize();
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testMissingUser() throws Exception {
+        final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.getSubcontext(TOTPContext.class, true);
+        
+        validator.initialize();
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+
+    @Test public void testMissingCode() throws Exception {
+        final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.getSubcontext(TOTPContext.class, true).setUsername("foo").getTokenSeeds().add("foo");
+        
+        validator.initialize();
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_CREDENTIALS);
+    }
+
+    @Test public void testMissingSeeds() throws Exception {
+        final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.getSubcontext(TOTPContext.class, true).setUsername("foo").setTokenCode(123456);
+        
+        validator.initialize();
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_CREDENTIALS);
+    }
+
+    @Test public void testUnmatchedUser() throws Exception {
+        final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.getSubcontext(TOTPContext.class, true).setUsername("bar").setTokenCode(123456).getTokenSeeds().add("foo");
+        
+        
+        validator.setMatchExpression(Pattern.compile("foo.+"));
+        validator.initialize();
+        
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.REQUEST_UNSUPPORTED);
+    }
+
+    @Test public void testInvalidSeed() throws Exception {
+        final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.getSubcontext(TOTPContext.class, true).setUsername("foo").setTokenCode(123456).getTokenSeeds().add("foo");
+        
+        validator.initialize();
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_CREDENTIALS);
+        AuthenticationErrorContext errorCtx = ac.getSubcontext(AuthenticationErrorContext.class);
+        Assert.assertTrue(errorCtx.getExceptions().get(0) instanceof IllegalArgumentException);
+    }
+
+    @Test public void testInvalidCode() throws Exception {
+        final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.getSubcontext(TOTPContext.class, true).setUsername("foo").setTokenCode(123456).getTokenSeeds().add("G24YUKCHHXRDWCPR");
+        
+        validator.initialize();
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_CREDENTIALS);
+        AuthenticationErrorContext errorCtx = ac.getSubcontext(AuthenticationErrorContext.class);
+        Assert.assertTrue(errorCtx.getExceptions().get(0) instanceof LoginException);
+    }
+
+    @Test public void testSuccess() throws Exception {
+        final AuthenticationContext ac = prc.getSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+
+        final GoogleAuthenticator auth = new GoogleAuthenticator();
+        final GoogleAuthenticatorKey creds = auth.createCredentials();
+
+        ac.getSubcontext(TOTPContext.class, true)
+            .setUsername("foo")
+            .setTokenCode(auth.getTotpPassword(creds.getKey()))
+            .getTokenSeeds().add(creds.getKey());
+
+        validator.initialize();
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        Assert.assertNotNull(ac.getAuthenticationResult());
+        Assert.assertEquals(ac.getAuthenticationResult().getSubject().getPrincipals(TOTPPrincipal.class).iterator()
+                .next().getName(), "foo");
+    }
+
+}
\ 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