TOTP (Google) Authenticator

Purpose

Support for TOTP Authenticator (such as Google and Microsoft Authenticator).

Implements RFC6238 (see https://tools.ietf.org/html/rfc6238) for Time-Based OTP devices.

Features

  • Secret generation
  • OTP code validation
  • QR Code generation for registration

Overview

The SMS Authentication plugin exists in 2 variants; dk.itp.security.authentication.googleauth.GoogleAuthAuthenticationPlugin which contains the logic for registering secrets and generating QR codes containing them, and a concrete implementation called dk.itp.portalprotect.useradmin.server.GoogleAuthUAAuthenticationPlugin which retrieves data from the Ceptor User Administration Server.

Configuration

The following configuration options exist for dk.itp.security.authentication.googleauth.GoogleAuthAuthenticationPlugin which handles the secret generation and TOTP code verification.

Property

Value

Description

totp.hashalgorithm

SHA1, SHA256 or SHA512

Default: SHA1

Specify the Hmac Algorithm to be used for verifying keys - note that some authenticators (including Google Authenticator only supports SHA1 so choose this with care.

Also note that even though SHA1 might be vulnerable, HmacSHA1 is not.

totp.issuerIssuer nameName of issuer - which is added to QR code - displayed by authenticator app
totp.period

Time period in seconds

Default: 30

Specify the time period between generating new codes - note that this is highly dependant upon the individual Authenticator app that is used if it works or not - Google Authenticator only supports 30 seconds.
totp.digits

Digis in code

Default: 6

Number of digits in the code that the authenticator app should show.

Note that google authenticator only supports 6 digit codes, so use with care.

totp.windowsize

Integer

Default: 3

Number of time periods to allow before or after the current time - setting this to 3 with a period of 30 seconds allows for clock skew between authenticator device and server of 90 seconds before or after current time.

Usually 3 is a sensible value.

totp.secretsize

Integer

Default: 20

Size of generated shared key - older versions of google authenticator seems to only support 10 characters / 80 bits - a more secure value is 20 characters which is 160 bits. Must be multiple of 5 to work properly with Base32 encoding, you should in general use 20 and only revert to 10 for compatibility reasons.


When using the version of the Google authentication plugin that uses the useradmin database; dk.itp.portalprotect.useradmin.server.GoogleAuthUAAuthenticationPlugin the following configuration properties exist in addition to the ones above:

Property

Value

Description

useradminservers

<url>

Default: localhost:15000

URL to useradmin server
ua_userid<userid>Userid to use when authenticating to useradmin server
ua_password<password>Password to use when authenticating to useradmin server
useridpassword.autounlockminutes

<value in minutes>

Default: 0

If nonzero, and user was automatically locked due to too many failed password attempts, he will automatically be unlocked after the specified number of minutes.
useridpassword.maximuminvalidpasswordattempts

<number>

Default: 0

If nonzero, and if invalid login attempts reaches this limit, the user is automatically locked.


Implementation in Your Custom Authentication Plugins

When doing authentication, you need a place to store the users secret between sessions, and you should also verify his password (assuming you use both userid/password and OTP to validate him) before using the secret.


This is an example authentication plugin that supports registering new OTP devices, generating new secrets and generating QR code for registration (Mobile Authenticators such as Google Authenticator supports scanning a QR code with the userid and secret embedded within).

The login in this plugin supports both two-step authentication where a users userid+password is validated first, then he is prompted for an OTP code, and the case where he is prompted from userid + password + OTP code in one go and all 3 are supplied and validated together.
The latter method is arguably more secure, since if either the OTP or password is wrong, you fail with the same error - if validating userid and password first, it allows guessing of the users password until additional measures are taken to limit the number of attempts.


package dk.portalprotect.nodb.plugins;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import dk.itp.security.authentication.googleauth.GoogleAuthAuthenticationPlugin;
import dk.itp.security.passticket.PTException;
import dk.itp.security.passticket.User;
import dk.itp.security.passticket.server.AuthErrorCodes;
import dk.itp.security.utils.Base64;

/**
 * Demonstration on how to create your own TOTP Authentication plugin
 *  
 * @author Kim Rasmussen
 *
 * <pre>
 * Ceptor - http://ceptor.io
 * 
 * This source code is confidential.
 * </pre>
 */
public class TOTPAuthenticationExample extends GoogleAuthAuthenticationPlugin {
	static Logger cat = LoggerFactory.getLogger(TOTPAuthenticationExample.class.getName());
	static final int DEFAULT_AUTHLEVEL = 10;

	public int getAuthenticationLevel() {
		return DEFAULT_AUTHLEVEL;
	}

	/**
	 * Generate new secret, or return PNG using QR code
	 */
	public String newToken(User user, String inputToken) throws PTException {
		String secret = "";

		try {
			if (!user.isLoggedOn) {
				// TODO: Here you can change the logic for registration / signing up new users
				throw new PTException("Not authenticated, cannot register new TOTP device", AuthErrorCodes.ERROR_NOTAUTHENTICATED, "Cannot register new OTP device until authenticated");
			}
			secret = (String)user.getStateObject("saved_totp_secret");
			if (secret == null) {
				// Create a new 
				secret = super.generateSecretSharedKey();
				user.setStateObject("saved_totp_secret", secret);
			}

			if (inputToken.equals("PNG"))
				return new String(Base64.encode(getQRPNG(secret, user.userid)), "UTF-8");
			else if (inputToken.equals("secret"))
				return secret;
			else
				throw new PTException("Invalid token");
		} catch(PTException e) {
			throw e;
		} catch(Exception e) {
			cat.error("Got exception trying to create new token for user " + user.userid, e);
			throw new PTException("Error trying to create new security token for user", AuthErrorCodes.ERROR_GENERALERROR, "Unknown error trying to create new token");
		}
	}

	public void login(User user, String userid, Object credentials) throws PTException {
		if (credentials instanceof String) {
			// TODO: Verify userid and password and lookup the shared secret, store the secret for use on next call
			//user.setStateObject("saved_totp_secret", secret_found_in_userstore);

			user.setStateObject("totp_password_verified", Boolean.TRUE);
			throw new PTException("Need OTP", AuthErrorCodes.ERROR_NEED_OTP, "Need OTP code");
		}

		if (credentials instanceof String[]) {
			String[] creds = (String[])credentials;

			String secret = (String)user.getStateObject("saved_totp_secret");

			long code = 0;
			try {
				if (creds.length == 2) {
					// Assume we are called with password and OTP - supporting login using userid + password + OTP all in one dialog

					// TODO: Verify password, lookup secret
					//String password = creds[0];

					// secret = secret_found_in_userstore;

					code = Long.parseLong(creds[1]);
				} else {
					// Assume we are just called with the OTP, supporting 2nd step login using authenticator code

					// Verify that we we called first
					if (!Boolean.TRUE.equals(user.getStateObject("totp_password_verified"))) {
						throw new PTException("Called with OTP without verifying first using userid+password");
					}

					code = Long.parseLong(creds[0]);
				}
			} catch(NumberFormatException e) {
				throw new PTException("Password does not match", AuthErrorCodes.ERROR_INVALIDCREDENTIALS, "Invalid credentials", e);
			}

			// Check the google authenticator code
			try {
				if (!verifyCode(secret, code)) {
					throw new PTException("OTP does not match", AuthErrorCodes.ERROR_INVALIDCREDENTIALS, "Invalid credentials");
				}
			} catch (InvalidKeyException | NoSuchAlgorithmException e) {
				throw new PTException("OTP does not match", AuthErrorCodes.ERROR_INVALIDCREDENTIALS, "Invalid credentials", e);
			}

			user.userid = userid;
			user.isLoggedOn = true;
		} else {
			cat.error("Cannot login, unsupported type of credentials");
			throw new PTException("Unsupported type of credentials - cannot login", AuthErrorCodes.ERROR_GENERALERROR, "Unsupported type of credentials");
		}
	}
}


Combining Authentication Plugins

If you have another existing plugin already, and you just want to reuse the functionality in the TOTP plugin (and the SMS plugin), your code can look similar to this to reuse just the SMS and TOTP functionality:

/**
 * 
 * This class extends the SMS authentication plugin to provide the same functionality that it has
 * regarding sending SMS messages to different supported providers.
 * 
 */
private class SMS extends SMSAuthenticationPlugin {
	/* (non-Javadoc)
	 * @see dk.itp.security.authentication.sms.SMSAuthenticationPlugin#generateOTP()
	 */
	@Override
	protected String generateOTP() {
		return super.generateOTP();
	}
	/* (non-Javadoc)
	 * @see dk.itp.security.authentication.sms.SMSAuthenticationPlugin#sendSMS(java.lang.String, java.lang.String)
	 */
	@Override
	protected boolean sendSMS(String phone, String text) throws MalformedURLException, IOException {
		return super.sendSMS(phone, text);
	}
}


/**
 * 
 * This class extends the TOTP/Google auth authentication plugin to provide the same functionality that it has
 * regarding generating and verifying OTP codes
 * 
 */
private class TOTP extends GoogleAuthAuthenticationPlugin {
	/* (non-Javadoc)
	 * @see dk.itp.security.authentication.googleauth.GoogleAuthAuthenticationPlugin#generateSecretSharedKey()
	 */
	@Override
	protected String generateSecretSharedKey() {
		return super.generateSecretSharedKey();
	}

	/* (non-Javadoc)
	 * @see dk.itp.security.authentication.googleauth.GoogleAuthAuthenticationPlugin#getQRPNG(java.lang.String, java.lang.String)
	 */
	@Override
	protected byte[] getQRPNG(String secret, String user) {
		return super.getQRPNG(secret, user);
	}
	/* (non-Javadoc)
	 * @see dk.itp.security.authentication.googleauth.GoogleAuthAuthenticationPlugin#verifyCode(java.lang.String, long)
	 */
	@Override
	protected boolean verifyCode(String secret, long code) throws NoSuchAlgorithmException, InvalidKeyException {
		return super.verifyCode(secret, code);
	}
}


If you add the classes above as inner classes in your own authentication plugin, you can easily create combinations, like first authenticating a user using userid and password, then allowing him to choose between 2nd factors, either a TOTP based authenticator such as Google Authenticator, of if he has lost his authenticator device, he can use SMS to get an OTP generated and sent to his phone.

You can contact support if you wish more examples of this.



© Ceptor ApS. All Rights Reserved.