[java-idp-plugin-totp] branch master updated: Initial flow design, extraction actions, some tests.

Scott Cantor cantor.2 at osu.edu
Wed Aug 5 22:38:04 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=780709c33a19529f239cc978b34eb968d96c301c

The following commit(s) were added to refs/heads/master by this push:
       new  780709c   Initial flow design, extraction actions, some tests.
780709c is described below

commit 780709c33a19529f239cc978b34eb968d96c301c
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Aug 5 18:39:17 2020 -0400

    Initial flow design, extraction actions, some tests.
---
 totp-impl/pom.xml                                  |   5 +
 .../totp/impl/AbstractTOTPCredentialValidator.java |  60 ++++++++-
 .../totp/impl/AbstractTOTPExtractionAction.java    | 135 +++++++++++++++++++++
 .../totp/impl/ExtractTOTPFromFormRequest.java      |  80 ++++++++++++
 .../plugin/totp/impl/ExtractTOTPFromHeader.java    |  80 ++++++++++++
 .../GoogleAuthenticatorCredentialValidator.java    |   2 +-
 .../shibboleth/idp/flows/authn/totp/totp-beans.xml |  53 ++++++++
 .../shibboleth/idp/flows/authn/totp/totp-flow.xml  |  52 ++++++++
 .../totp/impl/ExtractTOTPFromFormRequestTest.java  |  97 +++++++++++++++
 .../totp/impl/ExtractTOTPFromHeaderTest.java       |  97 +++++++++++++++
 ...GoogleAuthenticatorCredentialValidatorTest.java |   5 +
 11 files changed, 660 insertions(+), 6 deletions(-)

diff --git a/totp-impl/pom.xml b/totp-impl/pom.xml
index 386564b..4b494cb 100644
--- a/totp-impl/pom.xml
+++ b/totp-impl/pom.xml
@@ -64,6 +64,11 @@
             <artifactId>idp-authn-api</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-session-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
 
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
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
index e338552..b331029 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/AbstractTOTPCredentialValidator.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.plugin.totp.impl;
 
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.regex.Pattern;
 
@@ -35,9 +36,12 @@ 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.authn.context.SubjectCanonicalizationContext;
 import net.shibboleth.idp.plugin.totp.context.TOTPContext;
 import net.shibboleth.idp.plugin.totp.principal.TOTPPrincipal;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
@@ -56,6 +60,9 @@ public abstract class AbstractTOTPCredentialValidator extends AbstractCredential
     /** Lookup strategy for TOTP context. */
     @Nonnull private Function<AuthenticationContext,TOTPContext> totpContextLookupStrategy;
     
+    /** Source of token seeds. */
+    @NonnullAfterInit private Consumer<ProfileRequestContext> seedSource;
+        
     /** A regular expression to apply for acceptance testing. */
     @Nullable private Pattern matchExpression;
     
@@ -63,7 +70,7 @@ public abstract class AbstractTOTPCredentialValidator extends AbstractCredential
     public AbstractTOTPCredentialValidator() {
         totpContextLookupStrategy = new ChildContextLookup<>(TOTPContext.class);
     }
-    
+        
     /**
      * Set the lookup strategy to locate the {@link TOTPContext}.
      * 
@@ -73,7 +80,18 @@ public abstract class AbstractTOTPCredentialValidator extends AbstractCredential
             @Nonnull final Function<AuthenticationContext,TOTPContext> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
-        totpContextLookupStrategy = Constraint.isNotNull(strategy, "TOTPContextLookupStrategy cannot be null");
+        totpContextLookupStrategy = Constraint.isNotNull(strategy, "TOTPContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set source of token seeds.
+     * 
+     * @param source seed source
+     */
+    public void setSeedSource(@Nonnull final Consumer<ProfileRequestContext> source) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        seedSource = Constraint.isNotNull(source, "Token seed source cannot be null");
     }
     
     /**
@@ -86,7 +104,18 @@ public abstract class AbstractTOTPCredentialValidator extends AbstractCredential
         
         matchExpression = expression;
     }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
         
+        if (seedSource == null) {
+            throw new ComponentInitializationException("Token seed source cannot be null");
+        }
+    }
+
+// Checkstyle: CyclomaticComplexity OFF
     /** {@inheritDoc} */
     @Override
     protected Subject doValidate(@Nonnull final ProfileRequestContext profileRequestContext,
@@ -110,8 +139,8 @@ public abstract class AbstractTOTPCredentialValidator extends AbstractCredential
                         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());
+        } else if (totpContext.getTokenCode() == null) {
+            log.info("{} No tokencode available within TOTPContext", getLogPrefix());
             if (errorHandler != null) {
                 errorHandler.handleError(profileRequestContext, authenticationContext, (String) null,
                         AuthnEventIds.INVALID_CREDENTIALS);
@@ -119,6 +148,20 @@ public abstract class AbstractTOTPCredentialValidator extends AbstractCredential
             throw new LoginException(AuthnEventIds.INVALID_CREDENTIALS);
         }
         
+        if (totpContext.getTokenSeeds().isEmpty()) {
+            // Resolve seeds.
+            seedSource.accept(profileRequestContext);
+            
+            if (totpContext.getTokenSeeds().isEmpty()) {
+                log.info("{} No seeds were obtained for user '{}'", getLogPrefix(), totpContext.getUsername());
+                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;
@@ -126,7 +169,8 @@ public abstract class AbstractTOTPCredentialValidator extends AbstractCredential
                 
         return doValidate(profileRequestContext, authenticationContext, totpContext, warningHandler, errorHandler);
     }
-
+// Checkstyle: CyclomaticComplexity ON
+    
     /**
      * Override method for subclasses to use to perform the actual TOTP validation.
      * 
@@ -150,14 +194,20 @@ public abstract class AbstractTOTPCredentialValidator extends AbstractCredential
      * Decorate the subject with "standard" content from the validation.
      * 
      * @param subject the subject being returned
+     * @param profileRequestContext current profile request context
      * @param totpContext the TOTP context being validated
      * 
      * @return the decorated subject
      */
     @Nonnull protected Subject populateSubject(@Nonnull final Subject subject,
+            @Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final TOTPContext totpContext) {
         subject.getPrincipals().add(new TOTPPrincipal(totpContext.getUsername()));
         
+        // Bypass c14n. We already operate on a canonical name, so just re-confirm it.
+        profileRequestContext.getSubcontext(SubjectCanonicalizationContext.class, true).setPrincipalName(
+                totpContext.getUsername());
+        
         return super.populateSubject(subject);
     }
     
diff --git a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AbstractTOTPExtractionAction.java b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AbstractTOTPExtractionAction.java
new file mode 100644
index 0000000..9c9ec1d
--- /dev/null
+++ b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/AbstractTOTPExtractionAction.java
@@ -0,0 +1,135 @@
+/*
+ * 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 javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+
+import net.shibboleth.idp.authn.AbstractAuthenticationAction;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.totp.context.TOTPContext;
+import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
+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.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+/**
+ * An action that derives a username from a lookup strategy, a TOTP code from an arbitrary source,
+ * creates a {@link TOTPContext}, and attaches it to the {@link AuthenticationContext}.
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link AuthnEventIds#NO_CREDENTIALS}
+ * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null</pre>
+ */
+public abstract class AbstractTOTPExtractionAction extends AbstractAuthenticationAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractTOTPExtractionAction.class);
+    
+    /** Lookup strategy for username to use in resolving token seeds. */
+    @Nonnull private Function<ProfileRequestContext,String> usernameLookupStrategy;
+    
+    /** Creation strategy for TOTP context. */
+    @Nonnull private Function<AuthenticationContext,TOTPContext> totpContextCreationStrategy;
+    
+    /** TOTP context being operated on. */
+    @Nullable private TOTPContext totpContext;
+    
+    /** Constructor. */
+    public AbstractTOTPExtractionAction() {
+        usernameLookupStrategy = new CanonicalUsernameLookupStrategy();
+        totpContextCreationStrategy = new ChildContextLookup<>(TOTPContext.class, true);
+    }
+
+    /**
+     * Set the lookup strategy to use for the username to use in resolving token seeds.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setUsernameLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        usernameLookupStrategy = Constraint.isNotNull(strategy, "Username lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the lookup strategy to locate/create the {@link TOTPContext}.
+     * 
+     * @param strategy lookup/creation strategy
+     */
+    public void setTOTPContextCreationStrategy(@Nonnull final Function<AuthenticationContext,TOTPContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        totpContextCreationStrategy = Constraint.isNotNull(strategy, "TOTPContext creation strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+
+        totpContext = totpContextCreationStrategy.apply(authenticationContext);
+        totpContext.setTokenCode(null);
+        
+        // Fill in username if not set.
+        if (totpContext.getUsername() == null) {
+            final String username = usernameLookupStrategy.apply(profileRequestContext);
+            if (username == null) {
+                log.warn("{} No principal name available", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+                return;
+            }
+            totpContext.setUsername(username);
+        }
+        
+        final HttpServletRequest request = getHttpServletRequest();
+        if (request == null) {
+            log.debug("{} Profile action does not contain an HttpServletRequest", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+        
+        final Integer code = extractCode(request);
+        if (code == null) {
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+        
+        totpContext.setTokenCode(code);
+    }
+
+    /**
+     * Gets the token code from the HTTP request.
+     * 
+     * @param httpRequest current HTTP request
+     * 
+     * @return the token code, or null
+     */
+    @Nullable protected abstract Integer extractCode(@Nonnull final HttpServletRequest httpRequest);
+    
+}
\ No newline at end of file
diff --git a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/ExtractTOTPFromFormRequest.java b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/ExtractTOTPFromFormRequest.java
new file mode 100644
index 0000000..622f0a2
--- /dev/null
+++ b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/ExtractTOTPFromFormRequest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.servlet.http.HttpServletRequest;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+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;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An action that derives the TOTP code from a form parameter.
+ */
+public class ExtractTOTPFromFormRequest extends AbstractTOTPExtractionAction {
+
+    /** Default token code field name. */
+    @Nonnull @NotEmpty public static final String DEFAULT_FIELD_NAME = "tokencode";
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractTOTPFromFormRequest.class);
+    
+    /** Name of header. */
+    @NonnullAfterInit @NotEmpty private String fieldName;
+    
+    /** Constructor. */
+    public ExtractTOTPFromFormRequest() {
+        fieldName = DEFAULT_FIELD_NAME;
+    }
+    
+    /**
+     * Set the name of the field to examine.
+     * 
+     * @param field field name
+     */
+    public void setFieldName(@Nonnull @NotEmpty final String field) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        fieldName = Constraint.isNotNull(StringSupport.trimOrNull(field), "Field name cannot be null or empty");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected Integer extractCode(@Nonnull final HttpServletRequest httpRequest) {
+        final String code = httpRequest.getParameter(fieldName);
+        if (code != null) {
+            try {
+                return Integer.valueOf(code);
+            } catch (final NumberFormatException e) {
+                log.warn("{} Exception converting parameter value to integer code", getLogPrefix(), e);
+            }
+        } else {
+            log.trace("{} Token code field {} not found", getLogPrefix(), fieldName);
+        }
+        
+        return null;
+    }
+    
+}
\ No newline at end of file
diff --git a/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/ExtractTOTPFromHeader.java b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/ExtractTOTPFromHeader.java
new file mode 100644
index 0000000..d07fc0f
--- /dev/null
+++ b/totp-impl/src/main/java/net/shibboleth/idp/plugin/totp/impl/ExtractTOTPFromHeader.java
@@ -0,0 +1,80 @@
+/*
+ * 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.servlet.http.HttpServletRequest;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+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;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An action that derives the TOTP code from an HTTP header.
+ */
+public class ExtractTOTPFromHeader extends AbstractTOTPExtractionAction {
+
+    /** Default token code header. */
+    @Nonnull @NotEmpty public static final String DEFAULT_HEADER_NAME = "X-Shibboleth-TOTP";
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractTOTPFromHeader.class);
+    
+    /** Name of header. */
+    @NonnullAfterInit @NotEmpty private String headerName;
+    
+    /** Constructor. */
+    public ExtractTOTPFromHeader() {
+        headerName = DEFAULT_HEADER_NAME;
+    }
+    
+    /**
+     * Set the name of the header to examine.
+     * 
+     * @param header header name
+     */
+    public void setHeaderName(@Nonnull @NotEmpty final String header) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        headerName = Constraint.isNotNull(StringSupport.trimOrNull(header), "Header name cannot be null or empty");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected Integer extractCode(@Nonnull final HttpServletRequest httpRequest) {
+        final String code = httpRequest.getHeader(headerName);
+        if (code != null) {
+            try {
+                return Integer.valueOf(code);
+            } catch (final NumberFormatException e) {
+                log.warn("{} Exception converting header value to integer code", getLogPrefix(), e);
+            }
+        } else {
+            log.trace("{} Token code header {} not found", getLogPrefix(), headerName);
+        }
+        
+        return null;
+    }
+    
+}
\ No newline at end of file
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
index 02651ee..2f76f90 100644
--- 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
@@ -85,7 +85,7 @@ public class GoogleAuthenticatorCredentialValidator extends AbstractTOTPCredenti
             if (totpContext.getTokenSeeds().stream().anyMatch(
                     seed -> gAuth.authorize(seed, totpContext.getTokenCode()))) {
                 log.info("{} Login by '{}' succeeded", getLogPrefix(), totpContext.getUsername());
-                return populateSubject(new Subject(), totpContext);
+                return populateSubject(new Subject(), profileRequestContext, totpContext);
             }
             
             throw new LoginException(AuthnEventIds.INVALID_CREDENTIALS);
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
new file mode 100644
index 0000000..900b39c
--- /dev/null
+++ b/totp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/totp/totp-beans.xml
@@ -0,0 +1,53 @@
+<?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">
+       
+    <bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer"
+        p:placeholderPrefix="%{" p:placeholderSuffix="}" />
+
+    <bean class="net.shibboleth.ext.spring.config.IdentifiableBeanPostProcessor" />
+    <bean class="net.shibboleth.idp.profile.impl.ProfileActionBeanPostProcessor" />
+
+    <import resource="conditional:%{idp.home}/conf/authn/totp-authn-config.xml" />
+
+    <bean id="ExtractTOTPFromHeader"
+        class="net.shibboleth.idp.plugin.totp.impl.ExtractTOTPFromHeader" scope="prototype"
+        p:httpServletRequest-ref="shibboleth.HttpServletRequest"
+        p:headerName="#{getObject('shibboleth.authn.TOTP.HeaderName') ?: T(net.shibboleth.idp.plugin.totp.impl.ExtractTOTPFromHeader).DEFAULT_HEADER_NAME}" />
+
+    <bean id="ExtractTOTPFromFormRequest"
+        class="net.shibboleth.idp.plugin.totp.impl.ExtractTOTPFromFormRequest" scope="prototype"
+        p:httpServletRequest-ref="shibboleth.HttpServletRequest"
+        p:fieldName="#{getObject('shibboleth.authn.TOTP.FieldName') ?: T(net.shibboleth.idp.plugin.totp.impl.ExtractTOTPFromFormRequest).DEFAULT_FIELD_NAME}" />
+            
+    <bean id="ValidateTOTPCredentials"
+        class="net.shibboleth.idp.authn.impl.ValidateCredentials" scope="prototype"
+        p:validators="#{getObject('shibboleth.authn.TOTP.Validator') ?: getObject('DefaultTOTPValidator')}"
+        p:addDefaultPrincipals="#{getObject('shibboleth.authn.TOTP.addDefaultPrincipals') ?:
+            (getObject('shibboleth.authn.TOTP.PrincipalOverride') == null
+                or getObject('shibboleth.authn.TOTP.PrincipalOverride').isEmpty())}"
+        p:supportedPrincipals="#{getObject('shibboleth.authn.TOTP.PrincipalOverride')}"
+        p:classifiedMessages-ref="shibboleth.authn.TOTP.ClassifiedMessageMap"
+        p:resultCachingPredicate="#{getObject('shibboleth.authn.TOTP.resultCachingPredicate')}"
+        p:lockoutManager="#{getObject('shibboleth.authn.TOTP.AccountLockoutManager')}" />
+    
+    <!-- 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')}" />
+        
+    <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}" />
+
+</beans>
diff --git a/totp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/totp/totp-flow.xml b/totp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/totp/totp-flow.xml
new file mode 100644
index 0000000..153b84e
--- /dev/null
+++ b/totp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/totp/totp-flow.xml
@@ -0,0 +1,52 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+	parent="authn.abstract, authn/conditions">
+
+	<!-- This is a login flow for TOTP authentication -->
+
+    <!-- First check for HTTP header. -->
+    <action-state id="ExtractTOTPFromHeader">
+        <evaluate expression="ExtractTOTPFromHeader" />
+        <evaluate expression="'proceed'" />
+
+        <transition on="proceed" to="ValidateTOTPCredentials" />
+        <transition on="NoCredentials" to="ExtractTOTPFromFormRequest" />
+    </action-state>
+
+    <!-- Then check for propagation via password (or other) form. -->
+    <action-state id="ExtractTOTPFromFormRequest">
+        <evaluate expression="ExtractTOTPFromFormRequest" />
+        <evaluate expression="'proceed'" />
+
+        <transition on="proceed" to="ValidateTOTPCredentials" />
+        <transition on="NoCredentials" to="DisplayTOTPView" />
+    </action-state>
+
+	<view-state id="DisplayTOTPView" view="totp">
+		<on-render>
+			<evaluate expression="environment" result="viewScope.environment" />
+			<evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
+			<evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext))"
+				result="viewScope.authenticationContext" />
+			<evaluate expression="T(net.shibboleth.utilities.java.support.codec.HTMLEncoder)" result="viewScope.encoder" />
+			<evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
+			<evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
+			<evaluate
+				expression="flowRequestContext.getActiveFlow().getApplicationContext().containsBean('shibboleth.CustomViewContext') ? flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CustomViewContext') : null"
+				result="viewScope.custom" />
+		</on-render>
+		<transition on="proceed" to="ExtractTOTPFromFormRequest" />
+	</view-state>
+
+	<action-state id="ValidateTOTPCredentials">
+		<evaluate expression="ValidateTOTPCredentials" />
+		<evaluate expression="'proceed'" />
+
+        <transition on="proceed" to="proceed" />
+		<transition on="InvalidCredentials" to="DisplayTOTPView" />
+		<transition on="NoCredentials" to="DisplayTOTPView" />
+	</action-state>
+
+	<bean-import resource="totp-beans.xml" />
+
+</flow>
diff --git a/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/impl/ExtractTOTPFromFormRequestTest.java b/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/impl/ExtractTOTPFromFormRequestTest.java
new file mode 100644
index 0000000..2b0d2e0
--- /dev/null
+++ b/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/impl/ExtractTOTPFromFormRequestTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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 org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.impl.BaseAuthenticationContextTest;
+import net.shibboleth.idp.plugin.totp.context.TOTPContext;
+import net.shibboleth.idp.profile.ActionTestingSupport;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+
+/** {@link ExtractTOTPFromFormRequest} unit test. */
+public class ExtractTOTPFromFormRequestTest extends BaseAuthenticationContextTest {
+    
+    private ExtractTOTPFromFormRequest action; 
+    
+    @BeforeMethod public void setUp() throws Exception {
+        super.setUp();
+        
+        action = new ExtractTOTPFromFormRequest();
+        action.setFieldName("Foo");
+        action.setHttpServletRequest(new MockHttpServletRequest());
+        action.setUsernameLookupStrategy(FunctionSupport.constant("jdoe"));
+        action.initialize();
+    }
+    
+    @Test public void testNoServlet() throws Exception {
+        action = new ExtractTOTPFromFormRequest();
+        action.initialize();
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testNoUsername() throws Exception {
+        action = new ExtractTOTPFromFormRequest();
+        action.setUsernameLookupStrategy(FunctionSupport.constant(null));
+        action.initialize();
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testMissingField() throws Exception {
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testWrongField() throws Exception {
+        ((MockHttpServletRequest) action.getHttpServletRequest()).addParameter("Bar", "123456");
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testInvalidFormat() throws Exception {
+        ((MockHttpServletRequest) action.getHttpServletRequest()).addParameter("Foo", "A123456");
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testValid() throws Exception {
+        ((MockHttpServletRequest) action.getHttpServletRequest()).addParameter("Foo", "123456");
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        final AuthenticationContext authCtx = prc.getSubcontext(AuthenticationContext.class);
+        final TOTPContext totpCtx = authCtx.getSubcontext(TOTPContext.class);
+        Assert.assertNotNull(totpCtx);
+        Assert.assertEquals(totpCtx.getUsername(), "jdoe");
+        Assert.assertEquals(totpCtx.getTokenCode(), Integer.valueOf(123456));
+    }
+    
+}
\ No newline at end of file
diff --git a/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/impl/ExtractTOTPFromHeaderTest.java b/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/impl/ExtractTOTPFromHeaderTest.java
new file mode 100644
index 0000000..b8b3ad9
--- /dev/null
+++ b/totp-impl/src/test/java/net/shibboleth/idp/plugin/totp/impl/ExtractTOTPFromHeaderTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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 org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.impl.BaseAuthenticationContextTest;
+import net.shibboleth.idp.plugin.totp.context.TOTPContext;
+import net.shibboleth.idp.profile.ActionTestingSupport;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+
+/** {@link ExtractTOTPFromHeader} unit test. */
+public class ExtractTOTPFromHeaderTest extends BaseAuthenticationContextTest {
+    
+    private ExtractTOTPFromHeader action; 
+    
+    @BeforeMethod public void setUp() throws Exception {
+        super.setUp();
+        
+        action = new ExtractTOTPFromHeader();
+        action.setHeaderName("X-Foo");
+        action.setHttpServletRequest(new MockHttpServletRequest());
+        action.setUsernameLookupStrategy(FunctionSupport.constant("jdoe"));
+        action.initialize();
+    }
+    
+    @Test public void testNoServlet() throws Exception {
+        action = new ExtractTOTPFromHeader();
+        action.initialize();
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testNoUsername() throws Exception {
+        action = new ExtractTOTPFromHeader();
+        action.setUsernameLookupStrategy(FunctionSupport.constant(null));
+        action.initialize();
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testMissingHeader() throws Exception {
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testWrongHeader() throws Exception {
+        ((MockHttpServletRequest) action.getHttpServletRequest()).addHeader("Foo", "123456");
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testInvalidFormat() throws Exception {
+        ((MockHttpServletRequest) action.getHttpServletRequest()).addHeader("X-Foo", "A123456");
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testValid() throws Exception {
+        ((MockHttpServletRequest) action.getHttpServletRequest()).addHeader("X-Foo", "123456");
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        final AuthenticationContext authCtx = prc.getSubcontext(AuthenticationContext.class);
+        final TOTPContext totpCtx = authCtx.getSubcontext(TOTPContext.class);
+        Assert.assertNotNull(totpCtx);
+        Assert.assertEquals(totpCtx.getUsername(), "jdoe");
+        Assert.assertEquals(totpCtx.getTokenCode(), Integer.valueOf(123456));
+    }
+    
+}
\ 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
index fe8b3c1..7dd69de 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/GoogleAuthenticatorCredentialValidatorTest.java
@@ -52,8 +52,13 @@ public class GoogleAuthenticatorCredentialValidatorTest extends BaseAuthenticati
     @BeforeMethod public void setUp() throws Exception {
         super.setUp();
 
+        // We pre-populate the seeds for testing, but this is required for componnent init.
+        final StaticSeedSource seedsource = new StaticSeedSource();
+        seedsource.initialize();
+        
         validator = new GoogleAuthenticatorCredentialValidator();
         validator.setId("gauthtest");
+        validator.setSeedSource(seedsource);
         
         action = new ValidateCredentials();
         action.setValidators(Collections.singletonList(validator));

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


More information about the commits mailing list