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.issuer | Issuer name | Name 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.