SMS / Text OTP

Purpose

Generating One Time PIN/Password (OTP) and send them to end users using SMS/Text messages, as well as verifying the entered code. This enables 2-factor authentication.

Features

  • OTP code generation
  • OTP code validation
  • Send SMS using CPSMS provider
  • Send SMS using Unwire provider

Overview

The SMS Authentication plugin exists in 2 variants; 

  • dk.itp.security.authentication.sms.SMSAuthenticationPlugin - an abstract class which contains all the logic for issuing OTPs and validating them
  • dk.itp.portalprotect.useradmin.server.SMSUAAuthenticationPlugin - a concrete implementation which retrieves and validates challenges registered in the Ceptor User Administration Server. This implementation will also use the registered mobile number of the user to send the SMS/Text message

Configuration

This is the configuration for dk.itp.security.authentication.sms.SMSAuthenticationPlugin which is handles all the details.

PropertyValueDescription
sms.httpProxyHost

<hostname>

Default: blank

Hostname of HTTP proxy to use when contacting SMS Gateway, leave blank to skip proxy.
sms.httpProxyPort

<tcp port>

Default: 8080

TCP port number for proxy server.
sms.httpProxyUser

<userid>

Default: blank

If proxy is enabled, this is the userid to send, if specified
sms.httpProxyPassword

<password>

Default: blank

If proxy is enabled, and userid is specified, this is the password to send to the proxy server.
sms.httpNoProxyFor

<pattern>

No default

Hostnames/IPs matching this pattern will not be proxied
sms.from

<string>

Default: PP OTP

Name or phone number that the SMS/Text message will appear to be sent from.
sms.otplength

<integer>

Default: 6

Number of characters in the generated OTP code
sms.numbersonly

true/false

Default: true

If true, generated OTP only contains numbers - if false, it contains letters too.
sms.flashsms

true/false

Default: true

If true, the SMS/Text message is sent as flash SMS - meaning it will popup on the phone, and disappear when the user dismisses it - it will also not show up in the SMS history.
sms.verifysslhostname

true/false

Default: true

Set to false to disable SSL hostname verification - when true, it checks that the hostname matches what is specified in the SSL server certificate.
sms.verifysslservercert

true/false

Default: true

Set to false to disable verification of the SSL server certificate
sms.providercpssm/unwire

Name of the SMS provider to use, must be cpsms or unwire

An additional value of "locallogging" can be set to provide the OTP codes to the Ceptor log files.

When SMS provider is set to cpsms

sms.server

URL

Default: https://www.cpsms.dk

URL (scheme and hostname) of the SMS gateway server
sms.username<userid>Userid to send to the SMS gateway
sms.password<password>Password for the SMS gateway
sms.apikey<string>API Key to use instead of the userid/password when calling the gateway.
When SMS provider is set to unwire

sms.server

URL

Default: https://gw.unwire.com

URL (scheme and hostname) of the SMS gateway server
sms.username<userid>Userid to send to the SMS gateway
sms.password<password>Password for the SMS gateway
sms.smsc

<string>

Default: dk.tdc

Company to route the SMS through

Refer to unwire for details.

sms.appnr

<number>

Default: 1231

Number within the routing company to use for sending the SMS

Refer to unwire for details.

sms.mediacode

<string>

Default: PortalProtectOTP

Mediacode to use

Refer to unwire for details.


When using the version of the SMS authentication plugin that uses the useradmin database; dk.itp.portalprotect.useradmin.server.SMSUAAuthenticationPlugin  the following additional configuration properties exist:

PropertyValueDescription
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

This is an example of an implementation of your own authentication plugin, which inherits from the abstract one.

Please note that this example just covers

package dk.portalprotect.nodb.plugins;

import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Properties;

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

import dk.itp.security.authentication.sms.SMSAuthenticationPlugin;
import dk.itp.security.passticket.PTException;
import dk.itp.security.passticket.User;
import dk.itp.security.passticket.server.AuthErrorCodes;

/**
 * Examplle authentication plugin, used to demonstrate authenticating using SMS. 
 *  
 * @author Kim Rasmussen
 *
 * <pre>
 * Ceptor - http://ceptor.io
 * 
 * This source code is confidential.
 * </pre>
 */
public class SMSAuthenticationExample extends SMSAuthenticationPlugin {
	static Logger log = LoggerFactory.getLogger(SMSAuthenticationExample.class.getName());
	static final int DEFAULT_AUTHLEVEL = 10;

	/**
	 * @see dk.itp.security.authentication.IAuthenticationPlugin#getAuthenticationLevel()
	 */
	public int getAuthenticationLevel() {
		return DEFAULT_AUTHLEVEL;
	}

	/**
	 * @see dk.itp.security.authentication.IAuthenticationPlugin#login(User, String, Object, Object)
	 */
	public void login(User user, String userid, Object credentials) throws PTException {
		if (credentials instanceof String) {
			user.removeStateObject("SMS_OTP");
			user.removeStateObject("SMS_USERID");

			// Lookup userid in user repository, verify password
			// In this NON-PRODUCTION example, we assume the userid is the phone number, and the password is "password"

			if (!credentials.equals("password")) {
				throw new PTException("Invalid password", AuthErrorCodes.ERROR_INVALIDCREDENTIALS, "Invalid credentials");
			}

			// If we get here, the password is ok

			// For now, use the userid as phone number - in a real authentication plugin, look it up somewhere else.
			String mobile = userid;
			mobile = mobile == null ? "" : mobile.trim();

			if (mobile.startsWith("+"))
				mobile = mobile.substring(1);
			int ofs = mobile.indexOf(' ');
			while(ofs >= 0) {
				mobile = mobile.substring(0,ofs) + mobile.substring(ofs+1);
				ofs = mobile.indexOf(' ');
			}

			if (mobile == null || mobile.length() == 0)
				throw new PTException("No mobile number on record, cannot send PIN via SMS", AuthErrorCodes.ERROR_NOMOBILEPHONE, "No mobile number, cannot send SMS");

			// If 8 chars, assume danish and add country code 45 for Denmark
			if (mobile.length() == 8)
				mobile = "45"+mobile;

			// Generate an OTP, store it in the session, and throw an exception telling the application we need the OTP.
			String otp = generateOTP();

			user.setStateObject("SMS_OTP", otp);
			user.setStateObject("SMS_USERID", userid);

			try {
				if (!sendSMS(mobile, "PIN:\n"+otp))
					throw new PTException("Unable to send SMS to " + mobile, AuthErrorCodes.ERROR_NOMOBILEPHONE, "Problem with mobile, cannot send SMS");
			} catch (MalformedURLException e) {
				throw new PTException("Unable to send SMS to " + mobile, AuthErrorCodes.ERROR_NOMOBILEPHONE, "Configuration problem - invalid SMS configuration", e);
			} catch (IOException e) {
				throw new PTException("Unable to send SMS to " + mobile, AuthErrorCodes.ERROR_NOMOBILEPHONE, "Problem contacting SMS Gateway", e);
			}

			log.info("Logon with userid/password OK - SMS sent to " + mobile + ", need relogin with pincode");
			throw new PTException("Need OTP pincode", AuthErrorCodes.ERROR_NEED_OTP, "Need OTP code");

		} else if (credentials instanceof String[]) {
			// String[] means we are called with the one-time password.

			userid = (String) user.getStateObject("SMS_USERID");
			String otp = (String)user.getStateObject("SMS_OTP");

			if (userid == null || otp == null) {
				throw new PTException("Problem logging in, no userid present or timeout", AuthErrorCodes.ERROR_GENERALERROR, "No previous userid/password login or timeout");
			}

			// Remove allows only one try - keep it to allow multiple attempts at entering the correct PIN
			user.removeStateObject("SMS_OTP");
			user.removeStateObject("SMS_USERID");

			if (!((String[])credentials)[0].equalsIgnoreCase(otp)) {
				throw new PTException("Invalid OTP PIN", AuthErrorCodes.ERROR_INVALIDCREDENTIALS, "Invalid PIN code");
			}

			user.userid = userid;
			user.isLoggedOn = true;

			// Fill remaining information onto session
		}
		else {
			log.error("Cannot login, unsupported type of credentials");
			throw new PTException("Unsupported type of credentials - cannot login", AuthErrorCodes.ERROR_GENERALERROR, "Unsupported type of credentials");
		}
	}
	/**
	 * @see dk.itp.security.authentication.IAuthenticationPlugin#setConfiguration(Properties)
	 */
	public void setConfiguration(Properties props) {
		super.setConfiguration(props);

		log.error("Using Example SMS Plugin with hardcoded credentials - NOT FOR PRODUCTION");
	}
}



© Ceptor ApS. All Rights Reserved.