How to use OTP authentication

From SoFurry
Jump to: navigation, search

NOTE: OTP will be phased out in favor of OAuth 2.0 very soon!

SoFurry employs an OTP (One Time Password) scheme for applications accessing the site via the API. This document describes OTP Protocol version 2.1

Instead of regular http sessions, the client is expected to send special hashes with every request. These OTP-hashes changes with each request. This is done by incrementing a sequence counter by 1 at every request. You have to keep track of this sequence counter in your application.

On startup/initial connect you should do the following:

Step 1: MD5-Hash user password
Step 2: Set the sequence counter to 0, and the authentication padding to the default starting value ("@6F393fk6FzVz9aM63CfpsWE0J1Z7flEl9662X")
Step 3: concatenate the hashed user password, the authentication padding, and the current sequence counter to a string
Step 4: MD5-hash that concatenated string
Step 5: Send the request to the server

The server will reply in JSON format, and the variable "messageType" will tell you whether authentication was sucessful or not, as well as give you the current OTP sequence counter and new authentication padding which you have to set in your application. This is to prevent replay attacks so the otp sequence counter will not start at 0 after each login, but really increase every time. If you receive such an OTP counter update reply, you will have to re-send your request with the correct OTP sequence number as it's not been accepted yet!

So continuing the flow of events:

Step 6: Server replies with new OTP sequence, salt and authentication padding (Any operation requested by you is NOT executed yet)
Step 7: Your application repeats the request, this time with the new OTP sequence, salt and authentication padding.
Step 8-9999: Your application increases the OTP sequence every time, but keeps the salt and authentication padding the same.

NOTE: Authentication information on the server side is volatile, this means the server may throw away your authentication information at any point in time. If the server does that, the next request by you will fail, and you will essentially repeat the sequence starting at step 6, since the server will send you a new OTP counter value and a new authentication padding. You should not notify the user of re-authentication. This is a perfectly normal thing to happen, and there's no reason for the user to be notified about it. If authentication fails consecutively several times, the user's username or password is wrong. Due to the nature of this system the server is not able to discern between a wrong password and a wrong padding. The server will keep sending you a new OTP and Padding with every attempted request. Note that the server response will contain the version of the OTP protocol, which for example is "21" for version 2.1 - You can use this to determine compatibility and display a notification if the server version doesn't match what you expect.


With this system, neither the user's password nor their password hash is ever transmitted in the clear. The OTP sequence counter ensures that replay attacks will not work, since the hash will change with every request and an attacker has to have the user's private hash to be able to formulate a response that the server accepts.


Below is an Authentication class written in Java that shows a practical, working implementation. This example is targeted for android devices, but can be easily modified to work in any java compiler. The OTP interface can easily be implemented in any programming language. The only functions you need are MD5-hashing, sending of HTTP-POST-requests and the ability to parse JSON-responses from the server.


package com.playspoon;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;

import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.content.SharedPreferences;
import android.util.Log;

public class Authentication {

	private static String authenticationPadding = "@6F393fk6FzVz9aM63CfpsWE0J1Z7flEl9662X";
	private static long currentAuthenticationSequence = 0;
	private static String salt = "";

	//Ajax message types
	public static final int AJAXTYPE_APIERROR = 5;
	public static final int AJAXTYPE_OTPAUTH = 6;

	private static String username = null;
	private static String password = null;
	
	//Android-specific code for loading username and password from preferences storage
	public static void loadAuthenticationInformation(final Activity activity) {
	    SharedPreferences credentials = activity.getSharedPreferences(AppConstants.PREFS_NAME, 0);
	    username = credentials.getString("username", "");
	    password = credentials.getString("password", "");
	}

	//Android-specific code
	public static void savePreferences(final Activity activity) {
	      SharedPreferences settings = activity.getSharedPreferences(AppConstants.PREFS_NAME, 0);
	      SharedPreferences.Editor editor = settings.edit();
	      editor.putString("username", username);
	      editor.putString("password", password);
	      editor.commit();
	}

	//Get the MD5 sum of a given input string
	private static String getMd5Hash(final String input) {
		try {
			MessageDigest md = MessageDigest.getInstance("MD5");
			byte[] messageDigest = md.digest(input.getBytes());
			BigInteger number = new BigInteger(1, messageDigest);
			String md5 = number.toString(16);
			while (md5.length() < 32)
				md5 = "0" + md5;
			return md5;
		} catch (NoSuchAlgorithmException e) {
			Log.e("MD5", e.getMessage());
			return null;
		}
	}
	
	//Create a has using the current authentication sequence counter, thus "salting" the hash. 
	public static String generateRequestHash() {
		String hashedPassword = getMd5Hash(password+salt);
	    String hash = getMd5Hash(hashedPassword + authenticationPadding + currentAuthenticationSequence);
	    Log.d("Auth", "Password: "+hashedPassword+" padding: "+authenticationPadding+" sequence: "+currentAuthenticationSequence);
	    return hash;
	}

	
	public static Map<String, String> addAuthParametersToQuery(Map<String, String> queryParams) {
		queryParams.put("otpuser", username);
		queryParams.put("otphash", generateRequestHash());
		queryParams.put("otpsequence", ""+currentAuthenticationSequence);
		currentAuthenticationSequence = currentAuthenticationSequence+1;
		return queryParams;
	}
		
	public static long getCurrentAuthenticationSequence() {
		return currentAuthenticationSequence;
	}

	public static void setCurrentAuthenticationSequence(long newSequence) {
		currentAuthenticationSequence = newSequence;
	}

	public static void setCurrentAuthenticationPadding(String newPadding) {
		authenticationPadding = newPadding;
	}

	public static void setCurrentAuthenticationSalt(String newSalt) {
		salt = newSalt;
	}

	public static String getUsername() {
		return username;
	}

	public static String getPassword() {
		return password;
	}
	
	public static void updateAuthenticationInformation(Activity activity, String newUsername, String newPassword) {
		username = newUsername;
		password = newPassword;
		savePreferences(activity);
	}
	
	/**
	 * Check if passed json string contains data indicating a sequence mismatch, as well as the new sequence data
	 * @param httpResult
	 * @return true if no sequence data found or sequence correct, false if the request needs to be resent with the new enclosed sequence data
	 * @throws JSONException 
	 */
	public static boolean parseResponse(String httpResult) {
		try {
			//check for OTP sequence json and parse it.
			Log.d("Auth.parseResponse", "response: "+httpResult);
			JSONObject jsonParser;
			jsonParser = new JSONObject(httpResult);
			int messageType = jsonParser.getInt("messageType");
			if (messageType == AJAXTYPE_OTPAUTH) {
				int newSequence = jsonParser.getInt("newSequence");
				String newPadding = jsonParser.getString("newPadding");
				String newSalt = jsonParser.getString("newSalt");
				String otpVersion = jsonParser.getString("version");
				Log.d("Auth.parseResponse", "OTP Version: " + otpVersion + 
							"new Sequence: " + newSequence +
							" new Padding: " + newPadding +
							" new salt: " + newSalt );
				setCurrentAuthenticationSequence(newSequence);
				setCurrentAuthenticationPadding(newPadding);
				setCurrentAuthenticationSalt(newSalt);
				return false;
			}
		} catch (JSONException e) {
			Log.d("Auth.parseResponse", e.toString());
		}
		
		return true;
	}

}


Finally, here is some code showing the usage of above class. You have to send the request, and if you received a response telling you to update the OTP sequence counter, do that and resend the request.


	//Asynchronous http request and result parsing
	public void run() {
		try {
			HttpResponse response = HttpRequest.doPost(requestUrl, requestParameters);
			String httpResult = EntityUtils.toString(response.getEntity());
			numResults = 0;
			resultList = new ArrayList<String>();
			try {
				if (useAuthentication() && Authentication.parseResponse(httpResult) == false) {
					//Retry request with new otp sequence if it failed for the first time
					requestParameters = Authentication.addAuthParametersToQuery(originalRequestParameters);
					response = HttpRequest.doPost(requestUrl, requestParameters);
					httpResult = EntityUtils.toString(response.getEntity());
				}
				errorMessage  = parseErrorMessage(httpResult);
				if (errorMessage == null) {
					numResults = parseResponse(httpResult, resultList);
				}
			} catch (JSONException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

		} catch (ClientProtocolException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		handler.sendEmptyMessage(0);

	}




Javascript/jQuery example for the SoFurry 2.0 OTP API

Main difference in the 2.0 API is that the requests need to be sent to http://api2.sofurry.com

In this example, calling GetProfile() will fetch various profile data for a user. Note that while you can request profile data for any user on SF, you will only get the PM and notification counts for yourself.


var OTP_sequence = 0;
var OTP_pad = "ssdgljaeirngarHAIRFUAHRUaeifhawiohf"; //TODO: Randomize this!
var OTP_salt = "jeijWFEihsefIeh"; //TODO: Randomize this!
var requestRetries = 0;

// Get the Username from your setttings storage, this is just an example
var userId = "myuser";
// Get the password from your settings storage, this is just an example
var password = "mypassword";

function GetProfile() {


  // Build the JSON query URL
  var queryURL = "http://api2.sofurry.com/std/getUserProfile";
  var queryParameters = addOTPParameters(new array(), userId, password);
  requestRetries = 0;
  $.getJSON(queryURL, queryParameters, profileCallback);

}

//This function adds the needed OTP parameters to the request
function addOTPParameters(requestParameters, username, password) {
  requestParameters['otpuser'] = username;
  var hashedPassword = MD5(password + OTP_salt);
  var hash = MD5(hashedPassword + OTP_pad + OTP_sequence);
  requestParameters['otphash'] = hash;
  requestParameters['otpsequence'] = OTP_sequence;
  return requestParameters;
}


//--------------------------------------------
// Process the retrieved data
//--------------------------------------------
function profileCallback(data) {

	//We received new encryption credentials from the OTP service, so resend the request using the new data
	if (data.messageType != undefined && data.messageType == 6) {
		OTP_sequence = data.newSequence;
		OTP_pad = data.newPadding;
		OTP_salt = data.salt;
		var queryURL = "http://api2.sofurry.com/std/getUserProfile";
		var queryParameters = addOTPParameters(new array(), userId, password);
		requestRetries++;
		if (requestRetries > 5) {
			//Error, probably wrong password!
			return;
		}
		$.getJSON(queryURL, queryParameters, profileCallback);
		return;
	}
	// Avatar
	$("#avatar").empty();
	$("#avatar").append("<img src=\"http://www.sofurry.com/std/avatar?user="+data.userID+"\" width=\"50\">");
	// Profile home page URL
	$("#profileurl").empty();
	$("#profileurl").attr("href", "http://"+data.useralias+".sofurry.com").append(data.username);
	
	$("#pmcount").empty();
	$("#pmcount").attr("href", "http://www.sofurry.com/user/pm").append(data.unreadPMCount + " PMs");
	$("#notification_total").empty();
	$("#notification_total").attr("href", "http://www.sofurry.com/user/notification/list").append(data.notificationTotalCount + " notifications");
	$("#profileviews").empty();
	$("#profileviews").append("Profile views:<br>" + data.profileViewCount);
	$("#submissionviews").empty();
	$("#submissionviews").append("Submission views:<br>" + data.submissionViewCount);
	
}