Skip to content

Instantly share code, notes, and snippets.

@scruffyfox
Last active December 12, 2015 07:58
Show Gist options
  • Save scruffyfox/4740325 to your computer and use it in GitHub Desktop.
Save scruffyfox/4740325 to your computer and use it in GitHub Desktop.

Open source code for Authentication Activity used in the app Robin for App.net. A lot of users are concerned with the app using native password auth over OAuth. The app has been approved personally by the App.net staff which should be enough to trust the app in itself, however some users may not feel confident that the code being used for authentication is uncomprimisable.

The way authentication works is as followed:

  1. User enters username and password
  2. Username, password, required scopes, client token and password secret is converted to a byte array for post body using multipart/form-data content type.
  3. The body is posted over HTTPS to the authentication system

The only credentials the app stores is the access token which is required for things such as posting and doing basic API calls. User objects (Which contain usernames) are the only thing related to an account which is stored.

getConnectionInfo() returns an object which describes the information about a connection such as:

  1. response code
  2. response message
  3. response time
  4. connection URL
  5. connection body (which in this case would be a string value of a byte array, looking similar to [B@405217f8, no human readable data is presented here)
  6. connection time

APIManager uses my XLibrary class "AsyncHttpClient" for all http connections, can be found: https://github.com/scruffyfox/X-Library/blob/master/src/x/lib/AsyncHttpClient.java

Note: If you do manage to find a vulnerability or improvement that can be made, do email me.

/**
* Logs in the user
* @param username The user's username or email
* @param password The password
* @param c The <b>activity</b> context of the request
* @param response The response from the api call
*/
public synchronized void login(String username, String password, Context c, AsyncHttpResponse response)
{
HttpParams params = new HttpParams();
params.addParam("client_id", CLIENT_TOKEN);
params.addParam("password_grant_secret", PASSWORD_GRANT);
params.addParam("grant_type", "password");
params.addParam("scope", API_SCOPES);
params.addParam("username", username);
params.addParam("password", password);
byte[] byteData = AsyncHttpClient.getFormPostData(params);
mClient = new AsyncHttpClient();
mClient.post(API_AUTH, byteData, new HttpParams(AsyncHttpClient.getMultipartFormHeader()), response);
}
package in.rob.client;
import in.lib.Constants;
import in.lib.handler.LoginResponseHandler;
import in.lib.handler.UserResponseHandler;
import in.lib.handler.UserStreamResponseHandler;
import in.lib.manager.APIManager;
import in.lib.manager.ImageAPIManager;
import in.lib.manager.UserManager;
import in.obj.Auth;
import in.obj.User;
import in.rob.client.base.RobinActivity;
import in.rob.client.dialog.base.DialogBuilder;
import in.rob.client.page.UserFriendsPage;
import java.util.ArrayList;
import java.util.List;
import roboguice.inject.InjectView;
import x.lib.AsyncHttpClient;
import x.lib.AsyncHttpResponse;
import x.lib.CacheManager;
import x.type.HttpParams;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.assist.ImageLoadingListener;
import com.nullwire.trace.ExceptionHandler;
public class AuthenticateActivity extends RobinActivity implements OnEditorActionListener
{
@InjectView(R.id.username) private EditText mUsername;
@InjectView(R.id.password) private EditText mPassword;
@InjectView(R.id.beta_key) private EditText mBetakey;
@InjectView(R.id.login_button) private Button mLoginButton;
private ProgressDialog progress;
@Override public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.login_view);
if (getIntent() != null && ((MainApplication)getApplication()).isBeta())
{
if (getIntent().getData() != null)
{
Uri data = getIntent().getData();
mBetakey.setText(data.getQueryParameter("key"));
}
}
if (getIntent().getExtras() == null || !getIntent().getExtras().getBoolean(Constants.EXTRA_NEW_USER, false))
{
if (!TextUtils.isEmpty(UserManager.getAccessToken()))
{
startMainActivity();
finish();
return;
}
}
setTitle(R.string.login_title);
if (progress == null)
{
progress = new ProgressDialog(this);
progress.setMessage(getString(R.string.logging_in));
progress.setCanceledOnTouchOutside(false);
progress.setOnCancelListener(new OnCancelListener()
{
@Override public void onCancel(DialogInterface dialog)
{
APIManager.getInstance().cancel(getContext());
}
});
if (savedInstanceState != null && savedInstanceState.containsKey(Constants.EXTRA_SHOWING_PROGRESS))
{
if (savedInstanceState.getBoolean(Constants.EXTRA_SHOWING_PROGRESS))
{
progress.show();
}
}
}
if (((MainApplication)getApplication()).isBeta())
{
((View)mBetakey.getParent()).setVisibility(View.VISIBLE);
}
mPassword.setOnEditorActionListener(this);
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT)
{
InputMethodManager inputMethodManager = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.showSoftInput(mUsername, InputMethodManager.SHOW_FORCED);
}
}
@Override protected void onSaveInstanceState(Bundle outState)
{
outState.putBoolean(Constants.EXTRA_SHOWING_PROGRESS, progress.isShowing());
progress.dismiss();
super.onSaveInstanceState(outState);
}
/**
* Called when the login button is hit
* @param v The login button (dont use this as it can be called from keyboard IME)
*/
public void loginButtonClick(View v)
{
String usernameStr = mUsername.getText().toString();
String passwordStr = mPassword.getText().toString();
String betaStr = mBetakey.getText().toString();
mUsername.setHintTextColor(getResources().getColor(R.color.login_hint));
mPassword.setHintTextColor(getResources().getColor(R.color.login_hint));
mBetakey.setHintTextColor(getResources().getColor(R.color.login_hint));
if (TextUtils.isEmpty(usernameStr))
{
mUsername.setHintTextColor(getResources().getColor(R.color.login_hint_error));
return;
}
else
{
mUsername.setHintTextColor(getResources().getColor(R.color.login_hint));
}
if (TextUtils.isEmpty(passwordStr))
{
mPassword.setHintTextColor(getResources().getColor(R.color.login_hint_error));
return;
}
else
{
mPassword.setHintTextColor(getResources().getColor(R.color.login_hint));
}
if (((MainApplication)getApplication()).isBeta())
{
if (TextUtils.isEmpty(betaStr))
{
mBetakey.setHintTextColor(getResources().getColor(R.color.login_hint_error));
return;
}
else
{
mBetakey.setHintTextColor(getResources().getColor(R.color.login_hint));
}
progress.show();
checkBetaKeyLogin(betaStr, usernameStr, passwordStr);
return;
}
progress.show();
loginUser(usernameStr, passwordStr);
}
public void checkBetaKeyLogin(String betaKey, final String username, final String password)
{
AsyncHttpClient checker = new AsyncHttpClient();
JsonObject postData = new JsonObject();
postData.addProperty("token", betaKey);
postData.addProperty("username", username);
postData.addProperty("device_id", getDeviceId());
HttpParams headers = new HttpParams(new String[]{"Content-type", "application/json"});
checker.post(Constants.API_BETA_URL + Constants.API_BETA_CHECK, postData.toString(), headers, new AsyncHttpResponse()
{
@Override public void onSuccess(Object response)
{
SharedPreferences.Editor editor = getSharedPreferences(getPackageName(), Context.MODE_PRIVATE).edit();
editor.putBoolean(Constants.PREFS_HAS_BETA, true).apply();
loginUser(username, password);
}
@Override public void onFailure(int responseCode, String responseMessage)
{
progress.dismiss();
if (responseCode == 401)
{
DialogBuilder.create(getContext())
.setTitle(R.string.error)
.setMessage(R.string.beta_error)
.setPositiveButton(R.string.close, null)
.show();
}
else
{
DialogBuilder.create(getContext())
.setTitle(R.string.error)
.setMessage(R.string.login_error)
.setPositiveButton(R.string.close, null)
.show();
}
}
});
}
public void infoButtonClick(View v)
{
String information = "Robin is an App.net client and requires an account." +
"<br /><br />" +
"Robin uses password auth which has been approved by the staff of App.net and requires the permissions" +
"<br /><br />" +
"&#149; <b>Stream</b><br />Ability to read your stream<br /><br />" +
"&#149; <b>Email</b><br />Ability to access your account's email<br /><br />" +
"&#149; <b>Write post</b><br />Ability to write posts<br /><br />" +
"&#149; <b>Follow</b><br />Ability to follow/unfollow/mute users<br /><br />" +
"&#149; <b>Messages</b><br />Access to send and view private messages<br /><br />" +
"&#149; <b>Update profile</b><br />Ability to update your profile information<br /><br />" +
"&#149; <b>Files</b><br />Ability to retrive a list of your saved files<br /><br />" +
"App was created by @scruffyfox and @simpleline";
DialogBuilder.create(getContext())
.setTitle(R.string.information)
.setMessage(Html.fromHtml(information))
.setPositiveButton(R.string.close, null)
.show();
}
/**
* Initiates the login call and downloads the user's profile data
* @param username The username of the user logging in
* @param password The password of the user logging in
*/
public void loginUser(String username, String password)
{
APIManager.getInstance().login(username, password, getContext(), new LoginResponseHandler()
{
@Override public void onCallback()
{
downloadUser(getAccessToken(), getUserId());
}
@Override public void onFailure(int responseCode, String responseMessage)
{
progress.dismiss();
String errorMessage = getString(R.string.vague_error);
try
{
JsonElement data = new JsonParser().parse(responseMessage);
JsonObject returnedData = data.getAsJsonObject();
errorMessage = returnedData.get("error").getAsString();
}
catch (Exception e)
{
Exception e2 = new Exception(responseMessage + "\n" + getConnectionInfo());
ExceptionHandler.sendException(e2);
ExceptionHandler.sendException(e);
}
mUsername.setTextColor(getResources().getColor(R.color.login_text_error));
mPassword.setTextColor(getResources().getColor(R.color.login_text_error));
DialogBuilder.create(getContext())
.setTitle(getString(R.string.error))
.setMessage(errorMessage)
.setPositiveButton(getString(R.string.close), null)
.show();
}
});
}
/**
* Downloads the user profile and stores it in a cahced file
* @param accessToken The access token of the user
* @param userId The user id of the user
*/
public void downloadUser(final String accessToken, int userId)
{
Auth a = new Auth();
a.setAccessToken(accessToken);
UserManager.setAuth(a);
UserManager.setAccessToken(accessToken);
UserManager.setUserId("" + userId);
APIManager.getInstance().getUserDetails(null, "" + userId, new UserResponseHandler()
{
@Override public void onSuccess(byte[] content)
{
getUser().save(getApplicationContext());
UserManager.addUser(getUser(), UserManager.getAuth(), getApplicationContext());
UserManager.setUser(getUser(), getApplicationContext());
new NotificationReceiver().registerUserForPush(getApplicationContext());
getImageDelegateToken();
getFollowingList();
// Download the user's cover image
ImageLoader coverImageLoader = ImageLoader.getInstance();
DisplayImageOptions options = new DisplayImageOptions.Builder().cacheOnDisc().build();
coverImageLoader.displayImage(getUser().getCoverUrl(), new ImageView(getApplicationContext()), options, new ImageLoadingListener()
{
@Override public void onLoadingStarted(){}
@Override public void onLoadingCancelled(){}
@Override public void onLoadingFailed(FailReason failReason)
{
progress.dismiss();
startMainActivity();
}
@Override public void onLoadingComplete(Bitmap loadedImage)
{
progress.dismiss();
startMainActivity();
}
});
}
@Override public void onCallback(){}
});
}
/**
* Gets the image delegation token for the initial image service
*/
public void getImageDelegateToken()
{
ImageAPIManager.getInstance().registerForToken(getContext(), UserManager.getUser());
}
/**
* Used to build an initial list of users for the auto complete
*/
public void getFollowingList()
{
APIManager.getInstance().getUserFollowing(UserManager.getUserId(), "", new UserStreamResponseHandler()
{
@Override public void onCallback()
{
CacheManager cacheManager = new CacheManager(getContext(), getPackageName(), true);
List<User> users = (ArrayList<User>)cacheManager.readFile(Constants.CACHE_USERNAMES, new ArrayList<User>());
List<String> usersStr = (ArrayList<String>)cacheManager.readFile(Constants.CACHE_USERNAMES_STR, new ArrayList<String>());
for (User p : getUsers())
{
if (!usersStr.contains(p.getId()))
{
users.add(p);
usersStr.add(p.getId());
}
}
cacheManager.writeFile(String.format(Constants.CACHE_USER_LIST_NAME, UserFriendsPage.Mode.FOLLOWING.getModeText(), UserManager.getUserId()), (ArrayList<User>)getUsers());
cacheManager.writeFile(Constants.CACHE_USERNAMES, (ArrayList<User>)users);
cacheManager.writeFile(Constants.CACHE_USERNAMES_STR, (ArrayList<String>)usersStr);
users.clear();
usersStr.clear();
destroy();
}
});
}
/**
* Start the main activity
*/
public void startMainActivity()
{
Intent main = new Intent(this, MainActivity.class);
main.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
main.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
main.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
main.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(main);
finish();
}
@Override public boolean onCreateOptionsMenu(Menu menu)
{
getSupportMenuInflater().inflate(R.menu.empty, menu);
//menu.add(0, 1, 0, "Login").setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
return super.onCreateOptionsMenu(menu);
}
@Override public boolean onMenuItemSelected(int featureId, MenuItem item)
{
if (item.getItemId() == 1)
{
mLoginButton.performClick();
return true;
}
return super.onMenuItemSelected(featureId, item);
}
@Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event)
{
if (actionId == EditorInfo.IME_ACTION_GO)
{
mLoginButton.performClick();
return true;
}
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment