Add GitHub Login to an Android App
Or how I figured out dealing with incomplete documentation
I’m currently searching for a job as a Mobile Developer in the Geneva area so I’m applying to all sorts of positions and companies. One of them asked me to make a simple app that can browse a user’s repositories on GitHub, including authentication so you can see your own private repos (did you know we now have unlimited private repos for free on GitHub!). Here’s the finished code if you just want to have a look, but otherwise read on to see the necessary steps:
I have to preface this by saying that before this assignment I really had no idea how to use OAuth and I’ve only sent simple GET requests to APIs. I’m sure that for some people this article is just a whole lot of duh! but for me it was a learning experience and so I wanted to share it.
The first thing I did to start this app was, of course, to consult the GitHub API documentation:
The documentation exposes two main endpoints to get a list of repos:
/users/{username}/repos - for public repos
/user/repos - for logged in users
So let’s start with the simple use case:
Getting Public Repos
This is as easy as making a get request to the API and decoding the response. I’ve used OKHTTP inside an AsyncTask to make the request and then I simply decode it into an ArrayList of repo names (code is shortened and simplified):
Getting Private Repos (Basic Authentication)
In order to get private repos we have to log in, of course. And this is where the confusion started for me. The documentation talks a lot about OAuth2 and tokens and secrets and the only examples they give are using curl
. So I started searching the documentation and then online and lo and behold, Basic Authentication is a technical term! To summarize, basic authentication is essentially sending an Authorization
header which contains the string Basic
followed by a base64 encode of {username}:{password}.
So here’s how the code changes:
As you can see, not much changes, we simply change the endpoint and add that authorization header.
Getting Private Repos (Two Factor Authentication — OAuth2 Token)
This is where things get a bit more complex. The process itself isn’t too complicated once you understand how it works, which is like this: if 2FA is active, when you try to log in using basic authentication the API returns a 401 response and the error message Must specify two-factor authentication OTP code. At that point, if SMS 2FA is active, the platform should send out a OTP SMS to the user containing the code. Please note, however, that SMS 2FA for GitHub is finicky and might not work at all in some countries!
So to detect that we add the following in our try
block in doInBackground
:
if (response.code() == 401 && responseString.contains("Must specify two-factor authentication OTP code.")) return ERROR_2FA;
Since we’ve returned this we ask the user for their 2FA password. Technically speaking that’s all we need to access their repos list, but if we want to do more than that we need to acquire an authorization token from GitHub. For this, we need to send a login request to the authorizations endpoint:
private static final String GIT_HUB_API = "api.github.com";
private static final String AUTHORIZATIONS_ENDPOINT = "authorizations";Uri.Builder builder = new Uri.Builder();builder.scheme("https")
.authority(GIT_HUB_API)
.appendPath(AUTHORIZATIONS_ENDPOINT);//we start the AsyncTask passing the authHeader as built above
//and the 2FA OTP
new NetworkWork().execute(builder.build().toString(), authHeader, twoFA);
In order to request an authorization we must send a POST request in which we specify as JSON a title for the authorization. I found the easiest way to do this without using a try/catch block is by creating the JSON object from a HashMap. So we add this to the doInBackground
method of the AsyncTask:
if (url.length > 2) {
builder.addHeader("x-github-otp", url[2]);
MediaType JSON = MediaType.parse("application/json; charset=utf-8"); HashMap<String, String> map = new HashMap<>();
map.put("note", "GitHubExplorer);
JSONObject json = new JSONObject(map);
RequestBody body = RequestBody.create(json.toString(), JSON);
builder.post(body);
}
If the authentication is successful, the API will reply with a JSON containing a token
field with our authentication token. I have made a method to extract it and save it to SharedPreferences for easy access:
private void saveTwoFactorToken(String s) {
try {
JSONObject jsonRoot = new JSONObject(s);
String accessToken = jsonRoot.optString("token"); if (!TextUtils.isEmpty(accessToken)) {
SharedPreferences sharedPreferences = getPreferences(MODE_PRIVATE);
SharedPreferences.Editor sharedPreferencesEditor = sharedPreferences.edit();
sharedPreferencesEditor.putString(username, accessToken);
sharedPreferencesEditor.apply(); twoFactorToken = accessToken;
twoFactorRequested = false;
}
} catch (JSONException e) {
Toast.makeText(this, "Cannot decode 2FA JSON!", Toast.LENGTH_SHORT).show();
} //proceed with fetching data from API...
}
Afterwards, we can login as we do with Basic Authentication, the only difference being that instead of sending a header that contains:
Basic xxxx....
we send one that contains “token” instead of “Basic”, followed by the token of our authorization:
token {token}
And since I’m saving the token as SharedPreferences, we can easily check if we have a token for a certain username by using contains
:
SharedPreferences sharedPreferences = getPreferences(MODE_PRIVATE);if (sharedPreferences.contains(user)) {
//do something
}
And that’s it, we now have full access to the GitHub API! If you have any questions make sure to have a look at the full code on my GitHub page using the link at the top of the article or just leave a comment below!
Thanks for reading this article. You can connect with me on LinkedIn.
I’m currently looking for a job in the Geneva area, Switzerland, so please contact me if you know someone in need of a Mobile Developer with Java/Kotlin (Android) and JavaScript (React Native Android/iOS) experience.
If you liked this article, please hit the clap icon 👏 to show your support.