Difference between revisions of "How to use OTP authentication"
(Created page with 'SoFurry employs an OTP (One Time Password) scheme for applications accessing the site via the API. Instead of regular http sessions, an initial authentication handshake is being...') |
|||
Line 1: | Line 1: | ||
SoFurry employs an OTP (One Time Password) scheme for applications accessing the site via the API. | SoFurry employs an OTP (One Time Password) scheme for applications accessing the site via the API. | ||
− | Instead of regular http sessions, | + | 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: | On startup/initial connect you should do the following: |
Revision as of 17:12, 16 January 2010
SoFurry employs an OTP (One Time Password) scheme for applications accessing the site via the API.
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 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 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!
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.
NOTE: The authentication padding does not provide security. The current value is "@6F393fk6FzVz9aM63CfpsWE0J1Z7flEl9662X" and must be included in the string to be hashed.
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 final String authenticationPadding = "@6F393fk6FzVz9aM63CfpsWE0J1Z7flEl9662X"; private static long currentAuthenticationSequence = 0; //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); 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 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 == AppConstants.AJAXTYPE_OTPAUTH) { int newSequence = jsonParser.getInt("newSequence"); Log.d("Auth.parseResponse", "new Sequence: "+newSequence); setCurrentAuthenticationSequence(newSequence); 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); }