liuyuqi-dellpc 7 years ago
commit
aa1191a7a1

+ 31 - 0
.classpath

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" output="target/classes" path="src/main/java">
+		<attributes>
+			<attribute name="optional" value="true"/>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="src" output="target/test-classes" path="src/test/java">
+		<attributes>
+			<attribute name="optional" value="true"/>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="output" path="target/classes"/>
+</classpath>

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+/target
+/.settings
+/bin

+ 23 - 0
.project

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>googleAuthenticator</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.m2e.core.maven2Builder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.m2e.core.maven2Nature</nature>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>

+ 178 - 0
pom.xml

@@ -0,0 +1,178 @@
+<!--
+  ~ Copyright (c) 2014, 2015, Enrico Maria Crisostomo
+  ~ All rights reserved.
+  ~
+  ~ Redistribution and use in source and binary forms, with or without
+  ~ modification, are permitted provided that the following conditions are met:
+  ~
+  ~   * Redistributions of source code must retain the above copyright notice,
+  ~     this list of conditions and the following disclaimer.
+  ~
+  ~   * Redistributions in binary form must reproduce the above copyright notice,
+  ~     this list of conditions and the following disclaimer in the documentation
+  ~     and/or other materials provided with the distribution.
+  ~
+  ~   * Neither the name of the author nor the names of its
+  ~     contributors may be used to endorse or promote products derived from
+  ~     this software without specific prior written permission.
+  ~
+  ~ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+  ~ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  ~ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+  ~ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+  ~ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+  ~ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+  ~ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+  ~ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+  ~ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+  ~ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.warrenstrange</groupId>
+    <artifactId>googleauth</artifactId>
+    <version>0.4.5</version>
+    <packaging>jar</packaging>
+
+    <name>${project.groupId}:${project.artifactId}</name>
+    <description>GoogleAuth is a Java server library that implements the
+        Time-based One-time Password (TOTP) algorithm specified in RFC 6238.
+    </description>
+    <url>http://maven.apache.org</url>
+    <licenses>
+        <license>
+            <name>The BSD 3-Clause License</name>
+            <url>http://opensource.org/licenses/BSD-3-Clause</url>
+        </license>
+    </licenses>
+    <developers>
+        <developer>
+            <name>Enrico M. Crisostomo</name>
+            <email>enrico.m.crisostomo@gmail.com</email>
+            <organization>Enrico M. Crisostomo</organization>
+            <organizationUrl>http://thegreyblog.blogspot.com</organizationUrl>
+        </developer>
+    </developers>
+    <scm>
+        <connection>scm:git:git@github.com:wstrange/GoogleAuth.git</connection>
+        <developerConnection>scm:git:git@github.com:wstrange/GoogleAuth.git
+        </developerConnection>
+        <url>git@github.com:wstrange/GoogleAuth.git</url>
+    </scm>
+    <distributionManagement>
+        <snapshotRepository>
+            <id>ossrh</id>
+            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+        </snapshotRepository>
+    </distributionManagement>
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.12</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+            <version>1.10</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.4</version>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.2</version>
+                <configuration>
+                    <source>1.6</source>
+                    <target>1.6</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    <profiles>
+        <profile>
+            <id>release</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.sonatype.plugins</groupId>
+                        <artifactId>nexus-staging-maven-plugin</artifactId>
+                        <version>1.6.5</version>
+                        <extensions>true</extensions>
+                        <configuration>
+                            <serverId>ossrh</serverId>
+                            <nexusUrl>https://oss.sonatype.org/</nexusUrl>
+                            <autoReleaseAfterClose>true</autoReleaseAfterClose>
+                        </configuration>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-source-plugin</artifactId>
+                        <version>2.4</version>
+                        <executions>
+                            <execution>
+                                <id>attach-sources</id>
+                                <goals>
+                                    <goal>jar-no-fork</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-javadoc-plugin</artifactId>
+                        <!--
+                          This plugin chokes on self-closing tags, which are
+                          recreated automatically every time IDEA reformats the code.
+                        -->
+                        <configuration>
+                            <additionalparam>-Xdoclint:none</additionalparam>
+                        </configuration>
+                        <version>2.10.1</version>
+                        <executions>
+                            <execution>
+                                <id>attach-javadocs</id>
+                                <goals>
+                                    <goal>jar</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <!--
+                      The Maven GPG plugin relies on information found on
+                      settings.xml.
+                      See http://central.sonatype.org/pages/apache-maven.html for
+                      further information.
+                    -->
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-gpg-plugin</artifactId>
+                        <version>1.6</version>
+                        <executions>
+                            <execution>
+                                <id>sign-artifacts</id>
+                                <phase>verify</phase>
+                                <goals>
+                                    <goal>sign</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+</project>

+ 504 - 0
src/main/java/com/warrenstrange/googleauth/GoogleAuthenticator.java

@@ -0,0 +1,504 @@
+/*
+ * Copyright (c) 2014, 2015, Enrico Maria Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *
+ *   * Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *
+ *   * Neither the name of the author nor the names of its
+ *     contributors may be used to endorse or promote products derived from
+ *     this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.warrenstrange.googleauth;
+
+import org.apache.commons.codec.binary.Base32;
+import org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This class implements the functionality described in RFC 6238 (TOTP: Time
+ * based one-time password algorithm) and has been tested again Google's
+ * implementation of such algorithm in its Google Authenticator application.
+ * <p/>
+ * This class lets users create a new 16-bit base32-encoded secret key with
+ * the validation code calculated at time=0 (the UNIX epoch) and the URL of a
+ * Google-provided QR barcode to let an user load the generated information into
+ * Google Authenticator.
+ * <p/>
+ * This class doesn't store in any way either the generated keys nor the keys
+ * passed during the authorization process.
+ * <p/>
+ * Java Server side class for Google Authenticator's TOTP generator was inspired
+ * by an author's blog post.
+ *
+ * @author Enrico M. Crisostomo
+ * @author Warren Strange
+ * @version 1.0
+ * @see <a href="http://thegreyblog.blogspot.com/2011/12/google-authenticator-using-it-in-your.html" />
+ * @see <a href="http://code.google.com/p/google-authenticator" />
+ * @see <a href="http://tools.ietf.org/id/draft-mraihi-totp-timebased-06.txt" />
+ * @since 1.0
+ */
+public final class GoogleAuthenticator implements IGoogleAuthenticator {
+
+    /**
+     * The logger for this class.
+     */
+    private static final Logger LOGGER =
+            Logger.getLogger(GoogleAuthenticator.class.getName());
+
+    /**
+     * The number of bits of a secret key in binary form. Since the Base32
+     * encoding with 8 bit characters introduces an 160% overhead, we just need
+     * 80 bits (10 bytes) to generate a 16 bytes Base32-encoded secret key.
+     */
+    private static final int SECRET_BITS = 80;
+
+    /**
+     * Number of scratch codes to generate during the key generation.
+     * We are using Google's default of providing 5 scratch codes.
+     */
+    private static final int SCRATCH_CODES = 5;
+
+    /**
+     * Number of digits of a scratch code represented as a decimal integer.
+     */
+    private static final int SCRATCH_CODE_LENGTH = 8;
+
+    /**
+     * Modulus used to truncate the scratch code.
+     */
+    public static final int SCRATCH_CODE_MODULUS = (int) Math.pow(10, SCRATCH_CODE_LENGTH);
+
+    /**
+     * Magic number representing an invalid scratch code.
+     */
+    private static final int SCRATCH_CODE_INVALID = -1;
+
+    /**
+     * Length in bytes of each scratch code. We're using Google's default of
+     * using 4 bytes per scratch code.
+     */
+    private static final int BYTES_PER_SCRATCH_CODE = 4;
+
+    /**
+     * The SecureRandom algorithm to use.
+     *
+     * @see java.security.SecureRandom#getInstance(String)
+     */
+    @SuppressWarnings("SpellCheckingInspection")
+
+    private static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";
+
+    /**
+     * Sun random number algorithm provider name.
+     */
+    private static final String RANDOM_NUMBER_ALGORITHM_PROVIDER = "SUN";
+
+    /**
+     * Cryptographic hash function used to calculate the HMAC (Hash-based
+     * Message Authentication Code). This implementation uses the SHA1 hash
+     * function.
+     */
+    private static final String HMAC_HASH_FUNCTION = "HmacSHA1";
+
+    /**
+     * The configuration used by the current instance.
+     */
+    private final GoogleAuthenticatorConfig config;
+
+    /**
+     * The internal SecureRandom instance used by this class. Since as of Java 7
+     * Random instances are required to be thread-safe, no synchronisation is
+     * required in the methods of this class using this instance. Thread-safety
+     * of this class was a de-facto standard in previous versions of Java so
+     * that it is expected to work correctly in previous versions of the Java
+     * platform as well.
+     */
+    private ReseedingSecureRandom secureRandom = new ReseedingSecureRandom(
+            RANDOM_NUMBER_ALGORITHM,
+            RANDOM_NUMBER_ALGORITHM_PROVIDER);
+
+    public GoogleAuthenticator() {
+        config = new GoogleAuthenticatorConfig();
+    }
+
+    public GoogleAuthenticator(GoogleAuthenticatorConfig config) {
+        if (config == null) {
+            throw new IllegalArgumentException("Configuration cannot be null.");
+        }
+
+        this.config = config;
+    }
+
+    /**
+     * Calculates the verification code of the provided key at the specified
+     * instant of time using the algorithm specified in RFC 6238.
+     *
+     * @param key the secret key in binary format.
+     * @param tm  the instant of time.
+     * @return the validation code for the provided key at the specified instant
+     * of time.
+     */
+    int calculateCode(byte[] key, long tm) {
+        // Allocating an array of bytes to represent the specified instant
+        // of time.
+        byte[] data = new byte[8];
+        long value = tm;
+
+        // Converting the instant of time from the long representation to a
+        // big-endian array of bytes (RFC4226, 5.2. Description).
+        for (int i = 8; i-- > 0; value >>>= 8) {
+            data[i] = (byte) value;
+        }
+
+        // Building the secret key specification for the HmacSHA1 algorithm.
+        SecretKeySpec signKey = new SecretKeySpec(key, HMAC_HASH_FUNCTION);
+
+        try {
+            // Getting an HmacSHA1 algorithm implementation from the JCE.
+            Mac mac = Mac.getInstance(HMAC_HASH_FUNCTION);
+
+            // Initializing the MAC algorithm.
+            mac.init(signKey);
+
+            // Processing the instant of time and getting the encrypted data.
+            byte[] hash = mac.doFinal(data);
+
+            // Building the validation code performing dynamic truncation
+            // (RFC4226, 5.3. Generating an HOTP value)
+            int offset = hash[hash.length - 1] & 0xF;
+
+            // We are using a long because Java hasn't got an unsigned integer type
+            // and we need 32 unsigned bits).
+            long truncatedHash = 0;
+
+            for (int i = 0; i < 4; ++i) {
+                truncatedHash <<= 8;
+
+                // Java bytes are signed but we need an unsigned integer:
+                // cleaning off all but the LSB.
+                truncatedHash |= (hash[offset + i] & 0xFF);
+            }
+
+            // Clean bits higher than the 32nd (inclusive) and calculate the
+            // module with the maximum validation code value.
+            truncatedHash &= 0x7FFFFFFF;
+            truncatedHash %= config.getKeyModulus();
+
+            // Returning the validation code to the caller.
+            return (int) truncatedHash;
+        } catch (Exception ex) {
+            // Logging the exception.
+            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
+
+            // We're not disclosing internal error details to our clients.
+            throw new GoogleAuthenticatorException("The operation cannot be "
+                    + "performed now.");
+        }
+    }
+
+    /**
+     * This method implements the algorithm specified in RFC 6238 to check if a
+     * validation code is valid in a given instant of time for the given secret
+     * key.
+     *
+     * @param secret    the Base32 encoded secret key.
+     * @param code      the code to validate.
+     * @param timestamp the instant of time to use during the validation process.
+     * @param window    the window size to use during the validation process.
+     * @return <code>true</code> if the validation code is valid,
+     * <code>false</code> otherwise.
+     */
+    private boolean checkCode(
+            String secret,
+            long code,
+            long timestamp,
+            int window) {
+        byte[] decodedKey;
+
+        // Decoding the secret key to get its raw byte representation.
+        switch (config.getKeyRepresentation()) {
+            case BASE32:
+                Base32 codec32 = new Base32();
+                decodedKey = codec32.decode(secret);
+                break;
+            case BASE64:
+                Base64 codec64 = new Base64();
+                decodedKey = codec64.decode(secret);
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown key representation type.");
+        }
+
+        // convert unix time into a 30 second "window" as specified by the
+        // TOTP specification. Using Google's default interval of 30 seconds.
+        final long timeWindow = timestamp / this.config.getTimeStepSizeInMillis();
+
+        // Calculating the verification code of the given key in each of the
+        // time intervals and returning true if the provided code is equal to
+        // one of them.
+        for (int i = -((window - 1) / 2); i <= window / 2; ++i) {
+            // Calculating the verification code for the current time interval.
+            long hash = calculateCode(decodedKey, timeWindow + i);
+
+            // Checking if the provided code is equal to the calculated one.
+            if (hash == code) {
+                // The verification code is valid.
+                return true;
+            }
+        }
+
+        // The verification code is invalid.
+        return false;
+    }
+
+    @Override
+    public GoogleAuthenticatorKey createCredentials() {
+
+        // Allocating a buffer sufficiently large to hold the bytes required by
+        // the secret key and the scratch codes.
+        byte[] buffer =
+                new byte[SECRET_BITS / 8 + SCRATCH_CODES * BYTES_PER_SCRATCH_CODE];
+
+        secureRandom.nextBytes(buffer);
+
+        // Extracting the bytes making up the secret key.
+        byte[] secretKey = Arrays.copyOf(buffer, SECRET_BITS / 8);
+        String generatedKey = calculateSecretKey(secretKey);
+
+        // Generating the verification code at time = 0.
+        int validationCode = calculateValidationCode(secretKey);
+
+        // Calculate scratch codes
+        List<Integer> scratchCodes = calculateScratchCodes(buffer);
+
+        return new GoogleAuthenticatorKey(
+                generatedKey,
+                validationCode,
+                scratchCodes);
+    }
+
+    @Override
+    public GoogleAuthenticatorKey createCredentials(String userName) {
+        // Further validation will be performed by the configured provider.
+        if (userName == null) {
+            throw new IllegalArgumentException("User name cannot be null.");
+        }
+
+        GoogleAuthenticatorKey key = createCredentials();
+
+        ICredentialRepository repository = getValidCredentialRepository();
+        repository.saveUserCredentials(
+                userName,
+                key.getKey(),
+                key.getVerificationCode(),
+                key.getScratchCodes());
+
+        return key;
+    }
+
+    private List<Integer> calculateScratchCodes(byte[] buffer) {
+        List<Integer> scratchCodes = new ArrayList<Integer>();
+
+        while (scratchCodes.size() < SCRATCH_CODES) {
+            byte[] scratchCodeBuffer = Arrays.copyOfRange(
+                    buffer,
+                    SECRET_BITS / 8 + BYTES_PER_SCRATCH_CODE * scratchCodes.size(),
+                    SECRET_BITS / 8 + BYTES_PER_SCRATCH_CODE * scratchCodes.size() + BYTES_PER_SCRATCH_CODE);
+
+            int scratchCode = calculateScratchCode(scratchCodeBuffer);
+
+            if (scratchCode != SCRATCH_CODE_INVALID) {
+                scratchCodes.add(scratchCode);
+            } else {
+                scratchCodes.add(generateScratchCode());
+            }
+        }
+
+        return scratchCodes;
+    }
+
+    /**
+     * This method calculates a scratch code from a random byte buffer of
+     * suitable size <code>#BYTES_PER_SCRATCH_CODE</code>.
+     *
+     * @param scratchCodeBuffer a random byte buffer whose minimum size is
+     *                          <code>#BYTES_PER_SCRATCH_CODE</code>.
+     * @return the scratch code.
+     */
+    private int calculateScratchCode(byte[] scratchCodeBuffer) {
+        if (scratchCodeBuffer.length < BYTES_PER_SCRATCH_CODE) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "The provided random byte buffer is too small: %d.",
+                            scratchCodeBuffer.length));
+        }
+
+        int scratchCode = 0;
+
+        for (int i = 0; i < BYTES_PER_SCRATCH_CODE; ++i) {
+            scratchCode = (scratchCode << 8) + (scratchCodeBuffer[i] & 0xff);
+        }
+
+        scratchCode = (scratchCode & 0x7FFFFFFF) % SCRATCH_CODE_MODULUS;
+
+        // Accept the scratch code only if it has exactly
+        // SCRATCH_CODE_LENGTH digits.
+        if (validateScratchCode(scratchCode)) {
+            return scratchCode;
+        } else {
+            return SCRATCH_CODE_INVALID;
+        }
+    }
+
+    /* package */ boolean validateScratchCode(int scratchCode) {
+        return (scratchCode >= SCRATCH_CODE_MODULUS / 10);
+    }
+
+    /**
+     * This method creates a new random byte buffer from which a new scratch
+     * code is generated. This function is invoked if a scratch code generated
+     * from the main buffer is invalid because it does not satisfy the scratch
+     * code restrictions.
+     *
+     * @return A valid scratch code.
+     */
+    private int generateScratchCode() {
+        while (true) {
+            byte[] scratchCodeBuffer = new byte[BYTES_PER_SCRATCH_CODE];
+            secureRandom.nextBytes(scratchCodeBuffer);
+
+            int scratchCode = calculateScratchCode(scratchCodeBuffer);
+
+            if (scratchCode != SCRATCH_CODE_INVALID) {
+                return scratchCode;
+            }
+        }
+    }
+
+    /**
+     * This method calculates the validation code at time 0.
+     *
+     * @param secretKey The secret key to use.
+     * @return the validation code at time 0.
+     */
+    private int calculateValidationCode(byte[] secretKey) {
+        return calculateCode(secretKey, 0);
+    }
+
+    /**
+     * This method calculates the secret key given a random byte buffer.
+     *
+     * @param secretKey a random byte buffer.
+     * @return the secret key.
+     */
+    private String calculateSecretKey(byte[] secretKey) {
+        switch (config.getKeyRepresentation()) {
+            case BASE32:
+                return new Base32().encodeToString(secretKey);
+            case BASE64:
+                return new Base64().encodeToString(secretKey);
+            default:
+                throw new IllegalArgumentException("Unknown key representation type.");
+        }
+    }
+
+    @Override
+    public boolean authorize(String secret, int verificationCode)
+            throws GoogleAuthenticatorException {
+        // Checking user input and failing if the secret key was not provided.
+        if (secret == null) {
+            throw new IllegalArgumentException("Secret cannot be null.");
+        }
+
+        // Checking if the verification code is between the legal bounds.
+        if (verificationCode <= 0 || verificationCode >= this.config.getKeyModulus()) {
+            return false;
+        }
+
+        // Checking the validation code using the current UNIX time.
+        return checkCode(
+                secret,
+                verificationCode,
+                new Date().getTime(),
+                this.config.getWindowSize());
+    }
+
+    @Override
+    public boolean authorizeUser(String userName, int verificationCode)
+            throws GoogleAuthenticatorException {
+
+        ICredentialRepository repository = getValidCredentialRepository();
+
+        return authorize(repository.getSecretKey(userName), verificationCode);
+    }
+
+    /**
+     * This method loads the first available and valid ICredentialRepository
+     * registered using the Java service loader API.
+     *
+     * @return the first registered ICredentialRepository.
+     * @throws java.lang.UnsupportedOperationException if no valid service is
+     *                                                 found.
+     */
+    private ICredentialRepository getValidCredentialRepository() {
+        ICredentialRepository repository = getCredentialRepository();
+
+        if (repository == null) {
+            throw new UnsupportedOperationException(
+                    String.format("An instance of the %s service must be " +
+                                    "configured in order to use this feature.",
+                            ICredentialRepository.class.getName()
+                    )
+            );
+        }
+
+        return repository;
+    }
+
+    /**
+     * This method loads the first available ICredentialRepository
+     * registered using the Java service loader API.
+     *
+     * @return the first registered ICredentialRepository or <code>null</code>
+     * if none is found.
+     */
+    private ICredentialRepository getCredentialRepository() {
+        ServiceLoader<ICredentialRepository> loader =
+                ServiceLoader.load(ICredentialRepository.class);
+
+        //noinspection LoopStatementThatDoesntLoop
+        for (ICredentialRepository repository : loader) {
+            return repository;
+        }
+
+        return null;
+    }
+}

+ 150 - 0
src/main/java/com/warrenstrange/googleauth/GoogleAuthenticatorConfig.java

@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2014, Enrico Maria Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *
+ *   * Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *
+ *   * Neither the name of the author nor the names of its
+ *     contributors may be used to endorse or promote products derived from
+ *     this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.warrenstrange.googleauth;
+
+import java.util.concurrent.TimeUnit;
+
+public class GoogleAuthenticatorConfig {
+    private long timeStepSizeInMillis = TimeUnit.SECONDS.toMillis(30);
+    private int windowSize = 3;
+    private int codeDigits = 6;
+    private int keyModulus = (int) Math.pow(10, codeDigits);
+    private KeyRepresentation keyRepresentation = KeyRepresentation.BASE32;
+
+    /**
+     * Returns the key module.
+     *
+     * @return the key module.
+     */
+    public int getKeyModulus() {
+        return keyModulus;
+    }
+
+    /**
+     * Returns the key representation.
+     *
+     * @return the key representation.
+     */
+    public KeyRepresentation getKeyRepresentation() {
+        return keyRepresentation;
+    }
+
+    /**
+     * Returns the number of digits in the generated code.
+     *
+     * @return the number of digits in the generated code.
+     */
+    @SuppressWarnings("UnusedDeclaration")
+    public int getCodeDigits() {
+        return codeDigits;
+    }
+
+    /**
+     * Returns the time step size, in milliseconds, as specified by RFC 6238.
+     * The default value is 30.000.
+     *
+     * @return the time step size in milliseconds.
+     */
+    public long getTimeStepSizeInMillis() {
+        return timeStepSizeInMillis;
+    }
+
+    /**
+     * Returns an integer value representing the number of windows of size
+     * timeStepSizeInMillis that are checked during the validation process,
+     * to account for differences between the server and the client clocks.
+     * The bigger the window, the more tolerant the library code is about
+     * clock skews.
+     * <p/>
+     * We are using Google's default behaviour of using a window size equal
+     * to 3.  The limit on the maximum window size, present in older
+     * versions of this library, has been removed.
+     *
+     * @return the window size.
+     * @see #timeStepSizeInMillis
+     */
+    public int getWindowSize() {
+        return windowSize;
+    }
+
+    public static class GoogleAuthenticatorConfigBuilder {
+        private GoogleAuthenticatorConfig config = new GoogleAuthenticatorConfig();
+
+        public GoogleAuthenticatorConfig build() {
+            return config;
+        }
+
+        public GoogleAuthenticatorConfigBuilder setCodeDigits(int codeDigits) {
+            if (codeDigits <= 0) {
+                throw new IllegalArgumentException("Code digits must be positive.");
+            }
+
+            if (codeDigits < 6) {
+                throw new IllegalArgumentException("The minimum number of digits is 6.");
+            }
+
+            if (codeDigits > 8) {
+                throw new IllegalArgumentException("The maximum number of digits is 8.");
+            }
+
+            config.codeDigits = codeDigits;
+            config.keyModulus = (int) Math.pow(10, codeDigits);
+            return this;
+        }
+
+        public GoogleAuthenticatorConfigBuilder setTimeStepSizeInMillis(long timeStepSizeInMillis) {
+            if (timeStepSizeInMillis <= 0) {
+                throw new IllegalArgumentException("Time step size must be positive.");
+            }
+
+            config.timeStepSizeInMillis = timeStepSizeInMillis;
+            return this;
+        }
+
+        public GoogleAuthenticatorConfigBuilder setWindowSize(int windowSize) {
+            if (windowSize <= 0) {
+                throw new IllegalArgumentException("Window number must be positive.");
+            }
+
+            config.windowSize = windowSize;
+            return this;
+        }
+
+        public GoogleAuthenticatorConfigBuilder setKeyRepresentation(KeyRepresentation keyRepresentation) {
+            if (keyRepresentation == null) {
+                throw new IllegalArgumentException("Key representation cannot be null.");
+            }
+
+            config.keyRepresentation = keyRepresentation;
+            return this;
+        }
+    }
+}

+ 60 - 0
src/main/java/com/warrenstrange/googleauth/GoogleAuthenticatorException.java

@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2014, Enrico Maria Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *
+ *   * Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *
+ *   * Neither the name of the author nor the names of its
+ *     contributors may be used to endorse or promote products derived from
+ *     this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.warrenstrange.googleauth;
+
+/**
+ * Date: 12/02/14
+ * Time: 13:36
+ *
+ * @author Enrico M. Crisostomo
+ */
+public class GoogleAuthenticatorException extends RuntimeException {
+
+    /**
+     * Builds an exception with the provided error message.
+     *
+     * @param message the error message.
+     */
+    public GoogleAuthenticatorException(String message) {
+        super(message);
+    }
+
+    /**
+     * Builds an exception with the provided error mesasge and
+     * the provided cuase.
+     *
+     * @param message the error message.
+     * @param cause   the cause.
+     */
+    public GoogleAuthenticatorException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 134 - 0
src/main/java/com/warrenstrange/googleauth/GoogleAuthenticatorKey.java

@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2014, Enrico Maria Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *
+ *   * Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *
+ *   * Neither the name of the author nor the names of its
+ *     contributors may be used to endorse or promote products derived from
+ *     this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.warrenstrange.googleauth;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class is a JavaBean used by the GoogleAuthenticator library to represent
+ * a secret key.
+ * <p/>
+ * This class is immutable.
+ * <p/>
+ * Instance of this class should only be constructed by the GoogleAuthenticator
+ * library.
+ *
+ * @author Enrico M. Crisostomo
+ * @version 1.0
+ * @see GoogleAuthenticator
+ * @since 1.0
+ */
+public final class GoogleAuthenticatorKey {
+
+    /**
+     * The format string to generate the URL of a Google-provided QR bar code.
+     *
+     * @deprecated Use GoogleAuthenticatorQRGenerator instead.
+     */
+    private static final String QR_FORMAT =
+            "https://chart.googleapis.com/chart?chs=200x200&chld=M%%7C0&cht=qr&"
+                    + "chl=otpauth://totp/%s@%s%%3Fsecret%%3D%s";
+
+    /**
+     * The secret key in Base32 encoding.
+     */
+    private final String key;
+
+    /**
+     * The verification code at time = 0 (the UNIX epoch).
+     */
+    private final int verificationCode;
+
+    /**
+     * The list of scratch codes.
+     */
+    private final List<Integer> scratchCodes;
+
+    /**
+     * The constructor with package visibility.
+     *
+     * @param secretKey    the secret key in Base32 encoding.
+     * @param code         the verification code at time = 0 (the UNIX epoch).
+     * @param scratchCodes the list of scratch codes.
+     */
+    /* package */ GoogleAuthenticatorKey(
+            String secretKey, int code,
+            List<Integer> scratchCodes) {
+        key = secretKey;
+        verificationCode = code;
+        this.scratchCodes = new ArrayList<Integer>(scratchCodes);
+    }
+
+    /**
+     * Returns the URL of a Google-provided QR barcode to be loaded into the
+     * Google Authenticator application. The user scans this bar code with the
+     * application on their smart phones or manually enter the secret manually.
+     *
+     * @param user   the user to assign the secret key to.
+     * @param host   the host to assign the secret key to.
+     * @param secret the secret key in Base32 encoding.
+     * @return the URL of a Google-provided QR barcode to be loaded into the
+     * Google Authenticator application.
+     * @deprecated Use GoogleAuthenticatorQRGenerator##getOtpAuthURL instead.
+     */
+    @SuppressWarnings("deprecation, unused")
+    public static String getQRBarcodeURL(String user, String host, String secret) {
+        return String.format(QR_FORMAT, user, host, secret);
+    }
+
+    /**
+     * Get the list of scratch codes.
+     *
+     * @return the list of scratch codes.
+     */
+    public List<Integer> getScratchCodes() {
+        return scratchCodes;
+    }
+
+    /**
+     * Returns the secret key in Base32 encoding.
+     *
+     * @return the secret key in Base32 encoding.
+     */
+    public String getKey() {
+        return key;
+    }
+
+    /**
+     * Returns the verification code at time = 0 (the UNIX epoch).
+     *
+     * @return the verificationCode at time = 0 (the UNIX epoch).
+     */
+    public int getVerificationCode() {
+        return verificationCode;
+    }
+}

+ 185 - 0
src/main/java/com/warrenstrange/googleauth/GoogleAuthenticatorQRGenerator.java

@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 2014, Enrico Maria Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *
+ *   * Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *
+ *   * Neither the name of the author nor the names of its
+ *     contributors may be used to endorse or promote products derived from
+ *     this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.warrenstrange.googleauth;
+
+import org.apache.http.client.utils.URIBuilder;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
+/**
+ * This class provides helper methods to create a QR code containing the
+ * provided credential.  The generated QR code can be fed to the Google
+ * Authenticator application so that it can configure itself with the data
+ * contained therein.
+ */
+public final class GoogleAuthenticatorQRGenerator {
+    
+    /**
+     * The format string to generate the Google Chart HTTP API call.
+     */
+    private static final String TOTP_URI_FORMAT =
+            "https://chart.googleapis.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=%s";
+
+    /**
+     * This method wraps the invocation of <code>URLEncoder##encode</code>
+     * method using the "UTF-8" encoding.  This call also wraps the
+     * <code>UnsupportedEncodingException</code> thrown by
+     * <code>URLEncoder##encode</code> into a <code>RuntimeException</code>.
+     * Such an exception should never be thrown.
+     *
+     * @param s The string to URL-encode.
+     * @return the URL-encoded string.
+     */
+    private static String internalURLEncode(String s) {
+        try {
+            return URLEncoder.encode(s, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("UTF-8 encoding is not supported by URLEncoder.", e);
+        }
+    }
+
+    /**
+     * The label is used to identify which account a key is associated with.
+     * It contains an account name, which is a URI-encoded string, optionally
+     * prefixed by an issuer string identifying the provider or service managing
+     * that account.  This issuer prefix can be used to prevent collisions
+     * between different accounts with different providers that might be
+     * identified using the same account name, e.g. the user's email address.
+     * The issuer prefix and account name should be separated by a literal or
+     * url-encoded colon, and optional spaces may precede the account name.
+     * Neither issuer nor account name may themselves contain a colon.
+     * Represented in ABNF according to RFC 5234:
+     * <p/>
+     * label = accountname / issuer (“:” / “%3A”) *”%20” accountname
+     *
+     * @see <a href="https://code.google.com/p/google-authenticator/wiki/KeyUriFormat">Google Authenticator - KeyUriFormat</a>
+     */
+    private static String formatLabel(String issuer, String accountName) {
+        if (accountName == null || accountName.trim().length() == 0) {
+            throw new IllegalArgumentException("Account name must not be empty.");
+        }
+        
+        StringBuilder sb = new StringBuilder();
+        if (issuer != null) {
+            if (issuer.contains(":")) {
+                throw new IllegalArgumentException("Issuer cannot contain the \':\' character.");
+            }
+
+            sb.append(issuer);
+            sb.append(":");
+        }
+
+        sb.append(accountName);
+
+        return sb.toString();
+    }
+
+    /**
+     * Returns the URL of a Google Chart API call to generate a QR barcode to
+     * be loaded into the Google Authenticator application.  The user scans this
+     * bar code with the application on their smart phones or enters the
+     * secret manually.
+     * <p/>
+     * The current implementation supports the following features:
+     * <ul>
+     * <li>Label, made up of an optional issuer and an account name.</li>
+     * <li>Secret parameter.</li>
+     * <li>Issuer parameter.</li>
+     * </ul>
+     *
+     * @param issuer      The issuer name. This parameter cannot contain the colon
+     *                    (:) character. This parameter can be null.
+     * @param accountName The account name. This parameter shall not be null.
+     * @param credentials The generated credentials.  This parameter shall not be null.
+     * @return the Google Chart API call URL to generate a QR code containing
+     * the provided information.
+     * @see <a href="https://code.google.com/p/google-authenticator/wiki/KeyUriFormat">Google Authenticator - KeyUriFormat</a>
+     */
+    public static String getOtpAuthURL(String issuer,
+                                       String accountName,
+                                       GoogleAuthenticatorKey credentials) {
+
+        return String.format(
+                TOTP_URI_FORMAT,
+                internalURLEncode(getOtpAuthTotpURL(issuer, accountName, credentials)));
+    }
+
+    /**
+     * Returns the basic otpauth TOTP URI. This URI might be sent to the user via email, QR code or some other method.
+     * Use a secure transport since this URI contains the secret.
+     * <p/>
+     * The current implementation supports the following features:
+     * <ul>
+     * <li>Label, made up of an optional issuer and an account name.</li>
+     * <li>Secret parameter.</li>
+     * <li>Issuer parameter.</li>
+     * </ul>
+     *
+     * @param issuer      The issuer name. This parameter cannot contain the colon
+     *                    (:) character. This parameter can be null.
+     * @param accountName The account name. This parameter shall not be null.
+     * @param credentials The generated credentials.  This parameter shall not be null.
+     * @return an otpauth scheme URI for loading into a client application.
+     * @see <a href="https://code.google.com/p/google-authenticator/wiki/KeyUriFormat">Google Authenticator - KeyUriFormat</a>
+     */
+    public static String getOtpAuthTotpURL(String issuer,
+                                           String accountName,
+                                           GoogleAuthenticatorKey credentials) {
+        
+        URIBuilder uri = new URIBuilder()
+            .setScheme("otpauth")
+            .setHost("totp")
+            .setPath("/" + formatLabel(issuer, accountName))
+            .setParameter("secret", credentials.getKey());
+
+
+        if (issuer != null) {
+            if (issuer.contains(":")) {
+                throw new IllegalArgumentException("Issuer cannot contain the \':\' character.");
+            }
+
+            uri.setParameter("issuer", issuer);
+        }
+        
+        /*
+            The following parameters aren't needed since they are all defaults.
+            We can exclude them to make the URI shorter.
+         */
+        // uri.setParameter("algorithm", "SHA1");
+        // uri.setParameter("digits", "6");
+        // uri.setParameter("period", "30");
+        
+        return uri.toString();
+
+    }
+    
+}

+ 59 - 0
src/main/java/com/warrenstrange/googleauth/ICredentialRepository.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2014, Enrico Maria Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *
+ *   * Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *
+ *   * Neither the name of the author nor the names of its
+ *     contributors may be used to endorse or promote products derived from
+ *     this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.warrenstrange.googleauth;
+
+import java.util.List;
+
+/**
+ * @author Enrico M. Crisosotomo
+ */
+public interface ICredentialRepository {
+    /**
+     * This method retrieves the Base32-encoded private key of the given user.
+     *
+     * @param userName the user whose private key shall be retrieved.
+     * @return the private key of the specified user.
+     */
+    String getSecretKey(String userName);
+
+    /**
+     * This method saves the user credentials.
+     *
+     * @param userName       the user whose data shall be saved.
+     * @param secretKey      the generated key.
+     * @param validationCode the validation code.
+     * @param scratchCodes   the list of scratch codes.
+     */
+    void saveUserCredentials(String userName,
+                             String secretKey,
+                             int validationCode,
+                             List<Integer> scratchCodes);
+}

+ 101 - 0
src/main/java/com/warrenstrange/googleauth/IGoogleAuthenticator.java

@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2014, Enrico Maria Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *
+ *   * Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *
+ *   * Neither the name of the author nor the names of its
+ *     contributors may be used to endorse or promote products derived from
+ *     this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.warrenstrange.googleauth;
+
+/**
+ * Google Authenticator library interface.
+ */
+@SuppressWarnings("UnusedDeclaration")
+public interface IGoogleAuthenticator {
+    /**
+     * This method generates a new set of credentials including:
+     * <ol>
+     * <li>Secret key.</li>
+     * <li>Validation code.</li>
+     * <li>A list of scratch codes.</li>
+     * </ol>
+     * <p/>
+     * The user must register this secret on their device.
+     *
+     * @return secret key
+     */
+    GoogleAuthenticatorKey createCredentials();
+
+    /**
+     * This method generates a new set of credentials invoking the
+     * <code>#createCredentials</code> method with no arguments. The generated
+     * credentials are then saved using the configured
+     * <code>#ICredentialRepository</code> service.
+     * <p/>
+     * The user must register this secret on their device.
+     *
+     * @param userName the user name.
+     * @return secret key
+     */
+    GoogleAuthenticatorKey createCredentials(String userName);
+
+    /**
+     * Checks a verification code against a secret key using the current time.
+     * The algorithm also checks in a time window whose size determined by the
+     * <code>windowSize</code> property of this class.
+     * <p/>
+     * The default value of 30 seconds recommended by RFC 6238 is used for the
+     * interval size.
+     *
+     * @param secret           the Base32 encoded secret key.
+     * @param verificationCode the verification code.
+     * @return <code>true</code> if the validation code is valid,
+     * <code>false</code> otherwise.
+     * @throws GoogleAuthenticatorException if a failure occurs during the
+     *                                      calculation of the validation code.
+     *                                      The only failures that should occur
+     *                                      are related with the cryptographic
+     *                                      functions provided by the JCE.
+     */
+    boolean authorize(String secret, int verificationCode)
+            throws GoogleAuthenticatorException;
+
+    /**
+     * This method validates a verification code of the specified user whose
+     * private key is retrieved from the configured credential repository. This
+     * method delegates the validation to the <code>#authorize</code> method.
+     *
+     * @param userName         The user whose verification code is to be
+     *                         validated.
+     * @param verificationCode The validation code.
+     * @return <code>true</code> if the validation code is valid,
+     * <code>false</code> otherwise.
+     * @throws GoogleAuthenticatorException if an unexpected error occurs.
+     * @see #authorize(String, int)
+     */
+    boolean authorizeUser(String userName, int verificationCode)
+            throws GoogleAuthenticatorException;
+}

+ 35 - 0
src/main/java/com/warrenstrange/googleauth/KeyRepresentation.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2014, Enrico M. Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  Redistributions of source code must retain the above copyright notice, this
+ *   list of conditions and the following disclaimer.
+ *
+ *  Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ *
+ *  Neither the name of the author nor the names of its
+ *   contributors may be used to endorse or promote products derived from
+ *   this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.warrenstrange.googleauth;
+
+public enum KeyRepresentation {
+    BASE32,
+    BASE64
+}

+ 126 - 0
src/main/java/com/warrenstrange/googleauth/ReseedingSecureRandom.java

@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2014, 2015, Enrico Maria Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *
+ *   * Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *
+ *   * Neither the name of the author nor the names of its
+ *     contributors may be used to endorse or promote products derived from
+ *     this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.warrenstrange.googleauth;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Date: 08/04/14
+ * Time: 15:21
+ *
+ * @author Enrico M. Crisostomo
+ */
+class ReseedingSecureRandom {
+    private static final int MAX_OPERATIONS = 1000000;
+    private final String provider;
+    private final String algorithm;
+    private final AtomicInteger count = new AtomicInteger(0);
+    private SecureRandom secureRandom;
+
+    @SuppressWarnings("UnusedDeclaration")
+    ReseedingSecureRandom() {
+        this.algorithm = null;
+        this.provider = null;
+
+        buildSecureRandom();
+    }
+
+    @SuppressWarnings("UnusedDeclaration")
+    ReseedingSecureRandom(String algorithm) {
+        if (algorithm == null) {
+            throw new IllegalArgumentException("Algorithm cannot be null.");
+        }
+
+        this.algorithm = algorithm;
+        this.provider = null;
+
+        buildSecureRandom();
+    }
+
+    ReseedingSecureRandom(String algorithm, String provider) {
+        if (algorithm == null) {
+            throw new IllegalArgumentException("Algorithm cannot be null.");
+        }
+
+        if (provider == null) {
+            throw new IllegalArgumentException("Provider cannot be null.");
+        }
+
+        this.algorithm = algorithm;
+        this.provider = provider;
+
+        buildSecureRandom();
+    }
+
+    private void buildSecureRandom() {
+        try {
+            if (this.algorithm == null && this.provider == null) {
+                this.secureRandom = new SecureRandom();
+            } else if (this.provider == null) {
+                this.secureRandom = SecureRandom.getInstance(this.algorithm);
+            } else {
+                this.secureRandom = SecureRandom.getInstance(this.algorithm, this.provider);
+            }
+        } catch (NoSuchAlgorithmException e) {
+            throw new GoogleAuthenticatorException(
+                    String.format(
+                            "Could not initialise SecureRandom " +
+                                    "with the specified algorithm: %s",
+                            this.algorithm
+                    ), e
+            );
+        } catch (NoSuchProviderException e) {
+            throw new GoogleAuthenticatorException(
+                    String.format(
+                            "Could not initialise SecureRandom " +
+                                    "with the specified provider: %s",
+                            this.provider
+                    ), e
+            );
+        }
+    }
+
+    void nextBytes(byte[] bytes) {
+        if (count.incrementAndGet() > MAX_OPERATIONS) {
+            synchronized (this) {
+                if (count.get() > MAX_OPERATIONS) {
+                    buildSecureRandom();
+                    count.set(0);
+                }
+            }
+        }
+
+        this.secureRandom.nextBytes(bytes);
+    }
+}

+ 55 - 0
src/test/java/com/warrenstrange/googleauth/CredentialRepositoryMock.java

@@ -0,0 +1,55 @@
+package com.warrenstrange.googleauth;
+
+import java.util.List;
+
+/**
+ * A no-op implementation of the <code>#ICredentialRepository</code> interface.
+ *
+ * @author Enrico M. Crisostomo
+ */
+public class CredentialRepositoryMock implements ICredentialRepository {
+    /**
+     * Name of the environment property used by this mock to retrieve the fake
+     * secret key returned by <code>#getSecretKey</code>.
+     */
+    public static final String MOCK_SECRET_KEY_NAME =
+            "com.warrenstrange.googleauth.CredentialRepositoryMock.secret.name";
+
+    /**
+     * This method returns the value of the system property named
+     * <code>#MOCK_SECRET_KEY_NAME</code>.
+     *
+     * @param userName the user whose private key shall be retrieved.
+     * @return the value of the environment property named
+     * <code>#MOCK_SECRET_KEY_NAME</code>.
+     */
+    @Override
+    public String getSecretKey(String userName) {
+        final String key = System.getProperty(MOCK_SECRET_KEY_NAME);
+
+        System.out.println(
+                String.format(
+                        "getSecretKey invoked with user name %s returning %s.",
+                        userName,
+                        key));
+
+        return key;
+    }
+
+    /**
+     * This method does nothing.
+     *
+     * @param userName the user whose data shall be saved.
+     * @param secretKey the generated key.
+     * @param validationCode the validation code.
+     * @param scratchCodes the list of scratch codes.
+     */
+    @Override
+    public void saveUserCredentials(
+            String userName,
+            String secretKey,
+            int validationCode,
+            List<Integer> scratchCodes) {
+        System.out.println("saveUserCredentials invoked with user name " + userName);
+    }
+}

+ 178 - 0
src/test/java/com/warrenstrange/googleauth/GoogleAuthTest.java

@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2014, Enrico Maria Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   * Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *
+ *   * Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *
+ *   * Neither the name of the author nor the names of its
+ *     contributors may be used to endorse or promote products derived from
+ *     this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.warrenstrange.googleauth;
+
+import com.warrenstrange.googleauth.GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.math.BigInteger;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Not really a unit test, but it shows the basic usage of this package.
+ * To properly test the authenticator, manual intervention and multiple steps
+ * are required:
+ * <ol>
+ * <li>Run the test in order to generate the required information for a
+ * Google Authenticator application to be configured.</li>
+ * <li>Set the <code>SECRET_KEY</code> field with the value generated by the
+ * <code>GoogleAuthTest#createCredentials</code> method.</li>
+ * <li>Generate the current code with the Google Authenticator application and
+ * set the <code>VALIDATION_CODE</code> accordingly.</li>
+ * <li>Check that the <code>#authorise</code> method correctly validates the
+ * data when invoking the <code>GoogleAuthenticator#authorize</code> method.
+ * </li>
+ * </ol>
+ */
+public class GoogleAuthTest {
+
+    // Change this to the saved secret from the running the above test.
+    @SuppressWarnings("SpellCheckingInspection")
+    private static final String SECRET_KEY = "KR52HV2U5Z4DWGLJ";
+    private static final int VALIDATION_CODE = 598775;
+
+    @BeforeClass
+    public static void setupMockCredentialRepository() {
+        System.setProperty(
+                CredentialRepositoryMock.MOCK_SECRET_KEY_NAME,
+                SECRET_KEY);
+    }
+
+    private static byte[] hexStr2Bytes(String hex) {
+        // Adding one byte to get the right conversion
+        // Values starting with "0" can be converted
+        byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
+
+        // Copy all the REAL bytes, not the "first"
+        byte[] ret = new byte[bArray.length - 1];
+        for (int i = 0; i < ret.length; i++)
+            ret[i] = bArray[i + 1];
+        return ret;
+    }
+
+    @Test
+    public void rfc6238TestVectors() {
+        // See RFC 6238, p. 14
+        final String rfc6238TestKey = "3132333435363738393031323334353637383930";
+        final byte[] key = hexStr2Bytes(rfc6238TestKey);
+        final long testTime[] = {59L, 1111111109L, 1111111111L, 1234567890L, 2000000000L, 20000000000L};
+        final long testResults[] = {94287082, 7081804, 14050471, 89005924, 69279037, 65353130};
+        final long timeStepSizeInSeconds = 30;
+
+        GoogleAuthenticatorConfigBuilder cb = new GoogleAuthenticatorConfigBuilder();
+        cb.setCodeDigits(8).setTimeStepSizeInMillis(TimeUnit.SECONDS.toMillis(timeStepSizeInSeconds));
+        GoogleAuthenticator ga = new GoogleAuthenticator(cb.build());
+
+        for (int i = 0; i < testTime.length; ++i) {
+            assertEquals(ga.calculateCode(key, testTime[i] / timeStepSizeInSeconds), testResults[i]);
+        }
+    }
+
+    @Test
+    public void createCredentials() {
+        GoogleAuthenticatorConfigBuilder gacb =
+                new GoogleAuthenticatorConfigBuilder()
+                        .setKeyRepresentation(KeyRepresentation.BASE64);
+        GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator(gacb.build());
+
+        final GoogleAuthenticatorKey key =
+                googleAuthenticator.createCredentials();
+        final String secret = key.getKey();
+        final List<Integer> scratchCodes = key.getScratchCodes();
+
+        String otpAuthURL = GoogleAuthenticatorQRGenerator.getOtpAuthURL("Test Org.", "test@prova.org", key);
+
+        System.out.println("Please register (otpauth uri): " + otpAuthURL);
+        System.out.println("Base64-encoded secret key is " + secret);
+
+        for (Integer i : scratchCodes) {
+            if (!googleAuthenticator.validateScratchCode(i)) {
+                throw new IllegalArgumentException("An invalid code has been " +
+                        "generated: this is an application bug.");
+            }
+            System.out.println("Scratch code: " + i);
+        }
+    }
+
+    @Test
+    public void createCredentialsForUser() {
+        GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator();
+
+        final GoogleAuthenticatorKey key =
+                googleAuthenticator.createCredentials("testName");
+        final String secret = key.getKey();
+        final List<Integer> scratchCodes = key.getScratchCodes();
+
+        String otpAuthURL = GoogleAuthenticatorQRGenerator.getOtpAuthURL("Test Org.", "test@prova.org", key);
+
+        System.out.println("Please register (otpauth uri): " + otpAuthURL);
+        System.out.println("Secret key is " + secret);
+
+        for (Integer i : scratchCodes) {
+            if (!googleAuthenticator.validateScratchCode(i)) {
+                throw new IllegalArgumentException("An invalid code has been " +
+                        "generated: this is an application bug.");
+            }
+            System.out.println("Scratch code: " + i);
+        }
+    }
+
+    @Test
+    public void authorise() {
+        GoogleAuthenticatorConfigBuilder gacb =
+                new GoogleAuthenticatorConfigBuilder()
+                        .setTimeStepSizeInMillis(TimeUnit.SECONDS.toMillis(30))
+                        .setWindowSize(5);
+        GoogleAuthenticator ga = new GoogleAuthenticator(gacb.build());
+
+        boolean isCodeValid = ga.authorize(SECRET_KEY, VALIDATION_CODE);
+
+        System.out.println("Check VALIDATION_CODE = " + isCodeValid);
+    }
+
+    @Test
+    public void authoriseUser() {
+        GoogleAuthenticatorConfigBuilder gacb =
+                new GoogleAuthenticatorConfigBuilder()
+                        .setTimeStepSizeInMillis(TimeUnit.SECONDS.toMillis(30))
+                        .setWindowSize(5)
+                        .setCodeDigits(6);
+        GoogleAuthenticator ga = new GoogleAuthenticator(gacb.build());
+
+        boolean isCodeValid = ga.authorizeUser("testName", VALIDATION_CODE);
+
+        System.out.println("Check VALIDATION_CODE = " + isCodeValid);
+    }
+}

+ 36 - 0
src/test/java/com/warrenstrange/googleauth/GoogleAuthenticatorQRGeneratorTest.java

@@ -0,0 +1,36 @@
+package com.warrenstrange.googleauth;
+
+import junit.framework.TestCase;
+import org.junit.Test;
+
+import java.util.ArrayList;
+
+public class GoogleAuthenticatorQRGeneratorTest extends TestCase {
+
+	private GoogleAuthenticatorKey credentials;
+	
+	public void setUp() throws Exception {
+		super.setUp();
+		credentials = new GoogleAuthenticatorKey("secretKey", 123456, new ArrayList<Integer>() );
+	}
+	
+	@Test
+	public void testGetOtpAuthURL() throws Exception {
+		assertEquals("https://chart.googleapis.com/chart?chs=200x200&chld=M%7C0&cht=qr&chl=otpauth%3A%2F%2Ftotp%2FAcme%3Aalice%40example.com%3Fsecret%3DsecretKey%26issuer%3DAcme",
+				GoogleAuthenticatorQRGenerator.getOtpAuthURL("Acme", "alice@example.com", credentials));
+	}
+
+	@Test
+	public void testGetOtpAuthTotpURL() throws Exception {
+		assertEquals("otpauth://totp/Acme:alice@example.com?secret=secretKey&issuer=Acme",
+				GoogleAuthenticatorQRGenerator.getOtpAuthTotpURL("Acme", "alice@example.com", credentials));
+
+		// issuer and user with spaces
+		assertEquals("otpauth://totp/Acme%20Inc:alice%20at%20Inc?secret=secretKey&issuer=Acme+Inc",
+				GoogleAuthenticatorQRGenerator.getOtpAuthTotpURL("Acme Inc", "alice at Inc", credentials));
+
+		assertEquals("otpauth://totp/Acme%20&%20%3Cfriends%3E:alice%2523?secret=secretKey&issuer=Acme+%26+%3Cfriends%3E",
+				GoogleAuthenticatorQRGenerator.getOtpAuthTotpURL("Acme & <friends>", "alice%23", credentials));
+	}
+
+}

+ 1 - 0
src/test/resources/META-INF/services/com.warrenstrange.googleauth.ICredentialRepository

@@ -0,0 +1 @@
+com.warrenstrange.googleauth.CredentialRepositoryMock