[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