[java-idp-plugin-totp] branch master updated: Refactor with addition of TOTPAuthenticator interface.

Scott Cantor cantor.2 at osu.edu
Mon Aug 10 16:55:23 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=71215c7227edacf0bc3909bdf119adf3403de904

The following commit(s) were added to refs/heads/master by this push:
       new  71215c7   Refactor with addition of TOTPAuthenticator interface.
71215c7 is described below

commit 71215c7227edacf0bc3909bdf119adf3403de904
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Mon Aug 10 12:56:47 2020 -0400

    Refactor with addition of TOTPAuthenticator interface.
---
 .../idp/plugin/totp/context/TOTPContext.java       |  11 +-
 .../idp/plugin/totp/impl/AbstractSeedSource.java   |  37 ++++++
 .../totp/impl/AttributeResolverSeedSource.java     |  29 ++++-
 .../GoogleAuthenticatorCredentialValidator.java    | 102 ---------------
 .../plugin/totp/impl/GoogleTOTPAuthenticator.java  | 140 +++++++++++++++++++++
 .../idp/plugin/totp/impl/StaticSeedSource.java     |  26 +++-
 .../idp/plugin/totp/impl/TOTPAuthenticator.java    |  72 +++++++++++
 ...Validator.java => TOTPCredentialValidator.java} |  61 +++++----
 .../shibboleth/idp/flows/authn/TOTP/TOTP-beans.xml |  12 +-
 .../src/main/resources/conf/totp-authn-config.xml  |  26 ++++
 .../totp/impl/AttributeResolverSeedSourceTest.java |  19 ++-
 ...rTest.java => GoogleTOTPAuthenticatorTest.java} |  28 +++--
 12 files changed, 402 insertions(+), 161 deletions(-)

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 ccc8fd6..6205e16 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
@@ -42,11 +42,8 @@ public class TOTPContext extends BaseContext {
     /** The token code supplied. */
     @Nullable private Integer tokenCode;
 
-    /** The token seeds associated with the subject. */
-    @Nonnull @NonnullElements private Collection<String> tokenSeeds;
-    
-    /** URL for QR-code. */
-    @Nullable @NotEmpty private String totpURL;
+    /** The raw token seeds associated with the subject. */
+    @Nonnull @NonnullElements private Collection<byte[]> tokenSeeds;
 
     /** Constructor. */
     public TOTPContext() {
@@ -102,11 +99,11 @@ public class TOTPContext extends BaseContext {
     }
 
     /**
-     * Get the token seeds for the subject.
+     * Get the raw token seeds for the subject.
      *
      * @return the token seeds
      */
-     @Nonnull @NonnullElements @Live public Collection<String> getTokenSeeds() {
+     @Nonnull @NonnullElements @Live public Collection<byte[]> getTokenSeeds() {
          return tokenSeeds;
      }
 
diff --git a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AbstractSeedSource.java b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AbstractSeedSource.java
index 69c116a..24aaff2 100644
--- a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AbstractSeedSource.java
+++ b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AbstractSeedSource.java
@@ -40,14 +40,51 @@ import net.shibboleth.utilities.java.support.logic.FunctionSupport;
 public abstract class AbstractSeedSource extends AbstractInitializableComponent
         implements Consumer<ProfileRequestContext> {
 
+    /** Seed encoding types. */
+    public enum Encoding {
+        /** Use Base64 encoding. */
+        BASE64,
+        
+        /** Use Base32 encoding. */
+        BASE32,
+    };
+
     /** Lookup strategy for {@link TOTPContext}. */
     @Nonnull private Function<ProfileRequestContext,TOTPContext> totpContextLookupStrategy;
 
+    /** The encoding to reverse for the seed. */
+    @Nonnull private Encoding encoding;
+
     /** Constructor. */
     public AbstractSeedSource() {
         // PRC -> AuthenticationContext -> TOTPContext
         totpContextLookupStrategy = FunctionSupport.compose(new ChildContextLookup<>(TOTPContext.class),
                 new ChildContextLookup<>(AuthenticationContext.class));
+        
+        encoding = Encoding.BASE32;
+
+    }
+    
+    /**
+     * Get the encoding of the seeds once they're resolved.
+     * 
+     * <p>Defaults to {@link Encoding#BASE32}.</p>
+     * 
+     * @return encoding
+     */
+    @Nonnull public Encoding getEncoding() {
+        return encoding;
+    }
+    
+    /**
+     * Set the encoding of the seeds once they're resolved.
+     * 
+     * @param enc encoding
+     */
+    public void setEncoding(@Nonnull final Encoding enc) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        encoding = Constraint.isNotNull(enc, "Encoding cannot be null");
     }
 
     /**
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 011ee0a..74a76ba 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
@@ -35,6 +35,9 @@ 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.NotEmpty;
 import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.utilities.java.support.codec.Base32Support;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.DecodingException;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
@@ -49,7 +52,7 @@ public class AttributeResolverSeedSource extends AbstractSeedSource {
 
     /** Default attribute ID source. */
     @Nonnull @NotEmpty public static final String DEFAULT_ATTRIBUTE_ID = "tokenSeeds";
-    
+        
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(AttributeResolverSeedSource.class);
     
@@ -59,7 +62,6 @@ public class AttributeResolverSeedSource extends AbstractSeedSource {
     /** Attribute ID to resolve. */
     @NonnullAfterInit @NotEmpty private String attributeId;
     
-    
     /**
      * Set the {@link AttributeResolver} to use.
      * 
@@ -81,7 +83,7 @@ public class AttributeResolverSeedSource extends AbstractSeedSource {
         
         attributeId = Constraint.isNotNull(StringSupport.trimOrNull(id), "Source attribute ID cannot be null or empty");
     }
-    
+        
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -108,7 +110,7 @@ public class AttributeResolverSeedSource extends AbstractSeedSource {
 
             log.debug("Resolving attribute {} for '{}'", attributeId, totp.getUsername());
 
-            final Collection<String> seeds = totp.getTokenSeeds();
+            final Collection<byte[]> seeds = totp.getTokenSeeds();
 
             try {
                 // Resolve the attributes.
@@ -121,7 +123,24 @@ public class AttributeResolverSeedSource extends AbstractSeedSource {
                         .filter(StringAttributeValue.class::isInstance)
                         .map(StringAttributeValue.class::cast)
                         .map(StringAttributeValue::getValue)
-                        .forEachOrdered(seeds::add);
+                        .forEachOrdered(v -> {
+                            try {
+                                switch (getEncoding()) {
+                                    case BASE32:
+                                        seeds.add(Base32Support.decode(v));
+                                        break;
+                                        
+                                    case BASE64:
+                                        seeds.add(Base64Support.decode(v));
+                                        break;
+                                        
+                                    default:
+                                        throw new DecodingException("Unknown encoding type");
+                                }
+                            } catch (final DecodingException e) {
+                                log.error("Unable to decode seed", e);
+                            }
+                        });
                 }
             } finally {
                 totp.removeSubcontext(resCtx);
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
deleted file mode 100644
index 2f76f90..0000000
--- a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/GoogleAuthenticatorCredentialValidator.java
+++ /dev/null
@@ -1,102 +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.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(), profileRequestContext, 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/impl/GoogleTOTPAuthenticator.java b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/GoogleTOTPAuthenticator.java
new file mode 100644
index 0000000..3972b5d
--- /dev/null
+++ b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/GoogleTOTPAuthenticator.java
@@ -0,0 +1,140 @@
+/*
+ * 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.security.GeneralSecurityException;
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.utilities.java.support.codec.Base32Support;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.DecodingException;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.warrenstrange.googleauth.GoogleAuthenticator;
+import com.warrenstrange.googleauth.GoogleAuthenticatorConfig;
+import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
+
+/**
+ * A TOTP implementation based on Google Authenticator code.
+ */
+ at ThreadSafeAfterInit
+public class GoogleTOTPAuthenticator extends AbstractInitializableComponent implements TOTPAuthenticator {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(GoogleAuthenticator.class);
+    
+    /** Google Authenticator config. **/
+    @NonnullAfterInit private GoogleAuthenticatorConfig authconfig;
+
+    /** The implementation. */
+    @NonnullAfterInit private GoogleAuthenticator authenticator;
+    
+    /**
+     * Set the {@link GoogleAuthenticatorConfig} to use.
+     * 
+     * @param config configuration to use
+     */
+    public void setGoogleAuthenticatorConfig(@Nonnull final GoogleAuthenticatorConfig config) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        authconfig = Constraint.isNotNull(config, "GoogleAuthenticator cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (authconfig == null) {
+            authconfig = new GoogleAuthenticatorConfig();
+        }
+        authenticator = new GoogleAuthenticator(authconfig);
+    }
+
+    /** {@inheritDoc} */
+    public TOTPCredential createCredential() throws GeneralSecurityException {
+        
+        final byte[] secret;
+        final GoogleAuthenticatorKey cred = authenticator.createCredentials();
+        
+        try {
+            switch (authconfig.getKeyRepresentation()) {
+                case BASE32:
+                    secret = Base32Support.decode(cred.getKey());
+                    break;
+                    
+                case BASE64:
+                    secret = Base64Support.decode(cred.getKey());
+                    break;
+                    
+                default:
+                    throw new DecodingException("Unknown key representation type");
+            }
+            
+            return new TOTPCredential() {
+                public byte[] getKey() {
+                    return secret;
+                }
+    
+                public Collection<Integer> getScratchCodes() {
+                    return cred.getScratchCodes();
+                }
+            };
+        } catch (final DecodingException e) {
+            throw new GeneralSecurityException(e);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public boolean validate(@Nonnull @NotEmpty final byte[] secret, final int code) {
+        
+        final String encodedSecret;
+        
+        try {
+            switch(authconfig.getKeyRepresentation()) {
+                case BASE32:
+                    encodedSecret = Base32Support.encode(secret, false);
+                    break;
+                    
+                case BASE64:
+                    encodedSecret = Base64Support.encode(secret, false);
+                    break;
+                    
+                default:
+                    throw new EncodingException("Unknown key representation type");
+            }
+        } catch (final EncodingException e) {
+            return false;
+        }
+        
+        return authenticator.authorize(encodedSecret, code);
+    }
+
+}
\ No newline at end of file
diff --git a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/StaticSeedSource.java b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/StaticSeedSource.java
index c23eca7..5cd3f94 100644
--- a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/StaticSeedSource.java
+++ b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/StaticSeedSource.java
@@ -33,12 +33,17 @@ import com.google.common.collect.Multimap;
 import net.shibboleth.idp.plugin.totp.context.TOTPContext;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.utilities.java.support.codec.Base32Support;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.DecodingException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
 /**
  * Token seed source implementation that returns statically defined values.
+ * 
+ * <p>Values must be base32-encoded.</p>
  */
 @ThreadSafeAfterInit
 public class StaticSeedSource extends AbstractSeedSource {
@@ -47,7 +52,7 @@ public class StaticSeedSource extends AbstractSeedSource {
     @Nonnull private final Logger log = LoggerFactory.getLogger(StaticSeedSource.class);
     
     /** Token seeds. */
-    @Nonnull @NonnullElements private Multimap<String,String> tokenSeeds;
+    @Nonnull @NonnullElements private Multimap<String,byte[]> tokenSeeds;
     
     /** Constructor. */
     public StaticSeedSource() {
@@ -66,7 +71,24 @@ public class StaticSeedSource extends AbstractSeedSource {
         seeds.forEach((k,v) -> {
             final String user = StringSupport.trimOrNull(k);
             if (user != null) {
-                tokenSeeds.putAll(user, StringSupport.normalizeStringCollection(v));
+                StringSupport.normalizeStringCollection(v).forEach(s -> {
+                    try {
+                        switch (getEncoding()) {
+                            case BASE32:
+                                tokenSeeds.get(user).add(Base32Support.decode(s));
+                                break;
+                                
+                            case BASE64:
+                                tokenSeeds.get(user).add(Base64Support.decode(s));
+                                break;
+                                
+                            default:
+                                throw new DecodingException("Unknown encoding type");
+                        }
+                    } catch (final DecodingException e) {
+                        log.error("Unable to decode seed value", e);
+                    }
+                });
             }
         });
     }
diff --git a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/TOTPAuthenticator.java b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/TOTPAuthenticator.java
new file mode 100644
index 0000000..cde579a
--- /dev/null
+++ b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/TOTPAuthenticator.java
@@ -0,0 +1,72 @@
+/*
+ * 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.security.GeneralSecurityException;
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+
+/**
+ * Interface to a TOTP implementation's primitives.
+ */
+public interface TOTPAuthenticator {
+
+    /**
+     * Generate a new credential.
+     * 
+     * @return a new credential
+     * 
+     * @throws GeneralSecurityException if unable to generate the credential
+     */
+    @Nonnull TOTPCredential createCredential() throws GeneralSecurityException;
+    
+    /**
+     * Validate a secret and code.
+     * 
+     * @param secret token secret/seed, unencoded
+     * @param code token code to validate
+     * 
+     * @return true iff the code is valid
+     */
+    boolean validate(@Nonnull @NotEmpty final byte[] secret, final int code);
+    
+    /** Interface to a TOTP credential. */
+    public interface TOTPCredential {
+        
+        /**
+         * Get the secret key.
+         *
+         * @return the secret key
+         */
+        @Nonnull @NotEmpty byte[] getKey();
+
+        /**
+         * Get the list of scratch codes.
+         *
+         * @return the list of scratch codes
+         */
+        @Nonnull @NonnullElements @Unmodifiable @NotLive Collection<Integer> getScratchCodes();
+    }
+
+}
\ 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/TOTPCredentialValidator.java
similarity index 83%
rename from totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AbstractTOTPCredentialValidator.java
rename to totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/TOTPCredentialValidator.java
index d97edd9..08deaa9 100644
--- 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/TOTPCredentialValidator.java
@@ -32,7 +32,6 @@ 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;
@@ -46,20 +45,22 @@ 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.
+ * A {@link CredentialValidator} that checks for a {@link TOTPContext}.
  */
-public abstract class AbstractTOTPCredentialValidator extends AbstractCredentialValidator {
+public class TOTPCredentialValidator 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);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(TOTPCredentialValidator.class);
 
     /** Lookup strategy for TOTP context. */
     @Nonnull private Function<AuthenticationContext,TOTPContext> totpContextLookupStrategy;
     
+    /** TOTP implementation. */
+    @NonnullAfterInit private TOTPAuthenticator authenticator;
+    
     /** Source of token seeds. */
     @NonnullAfterInit private Consumer<ProfileRequestContext> seedSource;
         
@@ -67,7 +68,7 @@ public abstract class AbstractTOTPCredentialValidator extends AbstractCredential
     @Nullable private Pattern matchExpression;
     
     /** Constructor. */
-    public AbstractTOTPCredentialValidator() {
+    public TOTPCredentialValidator() {
         totpContextLookupStrategy = new ChildContextLookup<>(TOTPContext.class);
     }
         
@@ -83,6 +84,17 @@ public abstract class AbstractTOTPCredentialValidator extends AbstractCredential
         totpContextLookupStrategy = Constraint.isNotNull(strategy, "TOTPContext lookup strategy cannot be null");
     }
     
+    /**
+     * Set TOTP implementation to use.
+     * 
+     * @param impl TOTP implementation
+     */
+    public void setAuthenticator(@Nonnull final TOTPAuthenticator impl) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        authenticator = Constraint.isNotNull(impl, "TOTPAuthenticator cannot be null");
+    }
+    
     /**
      * Set source of token seeds.
      * 
@@ -167,28 +179,27 @@ public abstract class AbstractTOTPCredentialValidator extends AbstractCredential
             return null;
         }
                 
-        return doValidate(profileRequestContext, authenticationContext, totpContext, warningHandler, errorHandler);
+        log.debug("{} Attempting to authenticate token code for '{}' ", getLogPrefix(), totpContext.getUsername());
+        
+        try {
+            if (totpContext.getTokenSeeds().stream().anyMatch(
+                    seed -> authenticator.validate(seed, totpContext.getTokenCode()))) {
+                log.info("{} Login by '{}' succeeded", getLogPrefix(), totpContext.getUsername());
+                return populateSubject(new Subject(), profileRequestContext, totpContext);
+            }
+            
+            throw new LoginException(AuthnEventIds.INVALID_CREDENTIALS);
+        } catch (final Exception e) {
+            log.info("{} Login by '{}' failed", getLogPrefix(), totpContext.getUsername(), e);
+            if (errorHandler != null) { 
+                errorHandler.handleError(profileRequestContext, authenticationContext, e,
+                        AuthnEventIds.INVALID_CREDENTIALS);
+            }
+            throw e;
+        }
     }
 // Checkstyle: CyclomaticComplexity ON
     
-    /**
-     * 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.
diff --git a/totp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/TOTP/TOTP-beans.xml b/totp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/TOTP/TOTP-beans.xml
index 4a9cdfd..9fa48d9 100644
--- a/totp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/TOTP/TOTP-beans.xml
+++ b/totp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/TOTP/TOTP-beans.xml
@@ -39,10 +39,10 @@
         class="net.shibboleth.idp.authn.impl.ValidateCredentials" scope="prototype"
         p:validators="#{getObject('shibboleth.authn.TOTP.Validator') ?: getObject('DefaultTOTPValidator')}"
         p:resultCachingPredicate="#{getObject('shibboleth.authn.TOTP.resultCachingPredicate')}"
-        p:classifiedMessages-ref="TOTPClassifiedMessageMap"
+        p:classifiedMessages="#{getObject('shibboleth.authn.TOTP.ClassifiedMessageMap') ?: getObject('DefaultTOTPClassifiedMessageMap')}"
         p:lockoutManager="#{getObject('shibboleth.authn.TOTP.AccountLockoutManager')}" />
     
-    <util:map id="TOTPClassifiedMessageMap">
+    <util:map id="DefaultTOTPClassifiedMessageMap">
         <entry key="InvalidCredentials">
             <list>
                 <value>InvalidCredentials</value>
@@ -52,12 +52,14 @@
     
     <!-- These are singletons acting as default "back-ends". -->
     
-    <bean id="DefaultTOTPValidator" class="net.shibboleth.idp.plugin.totp.impl.GoogleAuthenticatorCredentialValidator" lazy-init="true"
-        p:matchExpression="#{getObject('shibboleth.authn.TOTP.matchExpression')}"
-        p:seedSource="#{getObject('shibboleth.authn.TOTP.SeedSource') ?: getObject('DefaultSeedSource')}" />
+    <bean id="DefaultTOTPValidator" class="net.shibboleth.idp.plugin.totp.impl.TOTPCredentialValidator" lazy-init="true"
+        p:seedSource="#{getObject('shibboleth.authn.TOTP.SeedSource') ?: getObject('DefaultSeedSource')}"
+        p:authenticator="#{getObject('shibboleth.authn.TOTP.Authenticator') ?: getObject('DefaultAuthenticator')}" />
         
     <bean id="DefaultSeedSource" class="net.shibboleth.idp.plugin.totp.impl.AttributeResolverSeedSource" lazy-init="true"
         p:attributeResolver-ref="shibboleth.AttributeResolverService"
         p:sourceAttribute="#{getObject('shibboleth.authn.TOTP.TokenSeedAttribute') ?: T(net.shibboleth.idp.plugin.totp.impl.AttributeResolverSeedSource).DEFAULT_ATTRIBUTE_ID}" />
 
+    <bean id="DefaultAuthenticator" class="net.shibboleth.idp.plugin.totp.impl.GoogleTOTPAuthenticator" lazy-init="true" />
+
 </beans>
diff --git a/totp-impl/src/main/resources/conf/totp-authn-config.xml b/totp-impl/src/main/resources/conf/totp-authn-config.xml
new file mode 100644
index 0000000..ab7a175
--- /dev/null
+++ b/totp-impl/src/main/resources/conf/totp-authn-config.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:context="http://www.springframework.org/schema/context"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xmlns:p="http://www.springframework.org/schema/p"
+       xmlns:c="http://www.springframework.org/schema/c"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+                           
+       default-init-method="initialize"
+       default-destroy-method="destroy">
+
+    <!-- Override default header/field extraction. -->
+    <!--
+    <bean id="shibboleth.authn.TOTP.HeaderName" class="java.lang.String" c:_0="X-Shibboleth-TOTP" />
+    <bean id="shibboleth.authn.TOTP.FieldName" class="java.lang.String" c:_0="tokencode" />
+    -->
+    
+    <!-- Override default attribute to resolve to locate token seeds for users. -->
+    <!--
+    <bean id="shibboleth.authn.TOTP.TokenSeedAttribute" class="java.lang.String" c:_0="tokenSeeds" />
+    -->
+    
+</beans>
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
index cf3e1e6..14cf07b 100644
--- 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
@@ -21,6 +21,7 @@ import static org.testng.Assert.*;
 
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 
 import javax.annotation.Nullable;
@@ -39,6 +40,8 @@ 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.codec.Base32Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.service.MockReloadableService;
 
@@ -48,14 +51,18 @@ public class AttributeResolverSeedSourceTest {
     private AttributeResolverSeedSource source;
     
     @BeforeMethod
-    public void setUp() throws ComponentInitializationException {
+    public void setUp() throws ComponentInitializationException, EncodingException {
+        
+        final String one = Base32Support.encode("one".getBytes(), false);
+        final String two = Base32Support.encode("two".getBytes(), false);
+        
         final IdPAttribute single = new IdPAttribute("single");
-        single.setValues(Collections.singletonList(StringAttributeValue.valueOf("one")));
+        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")));
+        multiple.setValues(List.of(StringAttributeValue.valueOf(one), StringAttributeValue.valueOf(null), StringAttributeValue.valueOf(two)));
         final AttributeDefinition multipledef = new MockAttributeDefinition("multiple", multiple);
         multipledef.initialize();
         
@@ -113,7 +120,7 @@ public class AttributeResolverSeedSourceTest {
         source.accept(prc);
         
         assertEquals(totp.getTokenSeeds().size(), 1);
-        assertEquals(totp.getTokenSeeds().iterator().next(), "one");
+        assertEquals(totp.getTokenSeeds().iterator().next(), "one".getBytes());
     }
 
     @Test public void multiple() throws ComponentInitializationException {
@@ -127,7 +134,9 @@ public class AttributeResolverSeedSourceTest {
         source.accept(prc);
         
         assertEquals(totp.getTokenSeeds().size(), 2);
-        assertEquals(totp.getTokenSeeds(), List.of("one", "two"));
+        final Iterator<byte[]> iter = totp.getTokenSeeds().iterator();
+        assertEquals(iter.next(), "one".getBytes());
+        assertEquals(iter.next(), "two".getBytes());
     }
 
     /**
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/GoogleTOTPAuthenticatorTest.java
similarity index 89%
rename from totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/impl/GoogleAuthenticatorCredentialValidatorTest.java
rename to totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/impl/GoogleTOTPAuthenticatorTest.java
index 16c8d41..d293e27 100644
--- 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/GoogleTOTPAuthenticatorTest.java
@@ -33,6 +33,7 @@ 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 net.shibboleth.utilities.java.support.codec.Base32Support;
 
 import org.springframework.webflow.execution.Event;
 import org.testng.Assert;
@@ -42,10 +43,12 @@ 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 {
+/** Unit test for {@link GoogleTOTPAuthenticator}. */
+public class GoogleTOTPAuthenticatorTest extends BaseAuthenticationContextTest {
     
-    private GoogleAuthenticatorCredentialValidator validator;
+    private GoogleTOTPAuthenticator authenticator;
+    
+    private TOTPCredentialValidator validator;
     
     private ValidateCredentials action;
 
@@ -56,9 +59,13 @@ public class GoogleAuthenticatorCredentialValidatorTest extends BaseAuthenticati
         final StaticSeedSource seedsource = new StaticSeedSource();
         seedsource.initialize();
         
-        validator = new GoogleAuthenticatorCredentialValidator();
+        authenticator = new GoogleTOTPAuthenticator();
+        authenticator.initialize();
+        
+        validator = new TOTPCredentialValidator();
         validator.setId("gauthtest");
         validator.setSeedSource(seedsource);
+        validator.setAuthenticator(authenticator);
         
         action = new ValidateCredentials();
         action.setValidators(Collections.singletonList(validator));
@@ -95,7 +102,7 @@ public class GoogleAuthenticatorCredentialValidatorTest extends BaseAuthenticati
     @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");
+        ac.getSubcontext(TOTPContext.class, true).setUsername("foo").getTokenSeeds().add("foo".getBytes());
         
         validator.initialize();
         action.initialize();
@@ -119,7 +126,7 @@ public class GoogleAuthenticatorCredentialValidatorTest extends BaseAuthenticati
     @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");
+        ac.getSubcontext(TOTPContext.class, true).setUsername("bar").setTokenCode(123456).getTokenSeeds().add("foo".getBytes());
         
         
         validator.setMatchExpression(Pattern.compile("foo.+"));
@@ -134,7 +141,7 @@ public class GoogleAuthenticatorCredentialValidatorTest extends BaseAuthenticati
     @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");
+        ac.getSubcontext(TOTPContext.class, true).setUsername("foo").setTokenCode(123456).getTokenSeeds().add("foo".getBytes());
         
         validator.initialize();
         action.initialize();
@@ -142,13 +149,14 @@ public class GoogleAuthenticatorCredentialValidatorTest extends BaseAuthenticati
         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);
+        Assert.assertTrue(errorCtx.getExceptions().get(0) instanceof LoginException);
     }
 
     @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");
+        ac.getSubcontext(TOTPContext.class, true).setUsername("foo").setTokenCode(123456).getTokenSeeds().add(
+                Base32Support.decode("G24YUKCHHXRDWCPR"));
         
         validator.initialize();
         action.initialize();
@@ -169,7 +177,7 @@ public class GoogleAuthenticatorCredentialValidatorTest extends BaseAuthenticati
         ac.getSubcontext(TOTPContext.class, true)
             .setUsername("foo")
             .setTokenCode(auth.getTotpPassword(creds.getKey()))
-            .getTokenSeeds().add(creds.getKey());
+            .getTokenSeeds().add(Base32Support.decode(creds.getKey()));
 
         validator.initialize();
         action.initialize();

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


More information about the commits mailing list