/* Copyright 2017 Andrew Dawson * * This file is part of Tusky. * * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * * You should have received a copy of the GNU General Public License along with Tusky. If not, see * . */ package com.keylesspalace.tusky; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import com.android.volley.Request; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.JsonObjectRequest; import org.json.JSONException; import org.json.JSONObject; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; public class LoginActivity extends AppCompatActivity { private static String OAUTH_SCOPES = "read write follow"; private SharedPreferences preferences; private String domain; private String clientId; private String clientSecret; /** * Chain together the key-value pairs into a query string, for either appending to a URL or * as the content of an HTTP request. */ private String toQueryString(Map parameters) throws UnsupportedEncodingException { StringBuilder s = new StringBuilder(); String between = ""; for (Map.Entry entry : parameters.entrySet()) { s.append(between); s.append(URLEncoder.encode(entry.getKey(), "UTF-8")); s.append("="); s.append(URLEncoder.encode(entry.getValue(), "UTF-8")); between = "&"; } return s.toString(); } /** Make sure the user-entered text is just a fully-qualified domain name. */ private String validateDomain(String s) { s = s.replaceFirst("http://", ""); s = s.replaceFirst("https://", ""); return s; } private String getOauthRedirectUri() { String scheme = getString(R.string.oauth_scheme); String host = getString(R.string.oauth_redirect_host); return scheme + "://" + host + "/"; } private void redirectUserToAuthorizeAndLogin() { /* To authorize this app and log in it's necessary to redirect to the domain given, * activity_login there, and the server will redirect back to the app with its response. */ String endpoint = getString(R.string.endpoint_authorize); String redirectUri = getOauthRedirectUri(); Map parameters = new HashMap<>(); parameters.put("client_id", clientId); parameters.put("redirect_uri", redirectUri); parameters.put("response_type", "code"); parameters.put("scope", OAUTH_SCOPES); String queryParameters; try { queryParameters = toQueryString(parameters); } catch (UnsupportedEncodingException e) { //TODO: No clue how to handle this error case?? assert(false); return; } String url = "https://" + domain + endpoint + "?" + queryParameters; Intent viewIntent = new Intent("android.intent.action.VIEW", Uri.parse(url)); startActivity(viewIntent); } /** * Obtain the oauth client credentials for this app. This is only necessary the first time the * app is run on a given server instance. So, after the first authentication, they are * saved in SharedPreferences and every subsequent run they are simply fetched from there. */ private void onButtonClick(final EditText editText) { domain = validateDomain(editText.getText().toString()); assert(domain != null); /* Attempt to get client credentials from SharedPreferences, and if not present * (such as in the case that the domain has never been accessed before) * authenticate with the server and store the received credentials to use next * time. */ clientId = preferences.getString(domain + "/client_id", null); clientSecret = preferences.getString(domain + "/client_secret", null); if (clientId != null && clientSecret != null) { redirectUserToAuthorizeAndLogin(); } else { String endpoint = getString(R.string.endpoint_apps); String url = "https://" + domain + endpoint; JSONObject parameters = new JSONObject(); try { parameters.put("client_name", getString(R.string.app_name)); parameters.put("redirect_uris", getOauthRedirectUri()); parameters.put("scopes", OAUTH_SCOPES); } catch (JSONException e) { //TODO: error text???? return; } JsonObjectRequest request = new JsonObjectRequest( Request.Method.POST, url, parameters, new Response.Listener() { @Override public void onResponse(JSONObject response) { try { clientId = response.getString("client_id"); clientSecret = response.getString("client_secret"); } catch (JSONException e) { //TODO: Heck return; } SharedPreferences.Editor editor = preferences.edit(); editor.putString(domain + "/client_id", clientId); editor.putString(domain + "/client_secret", clientSecret); editor.apply(); redirectUserToAuthorizeAndLogin(); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { editText.setError( "This app could not obtain authentication from that server " + "instance."); error.printStackTrace(); } }); VolleySingleton.getInstance(this).addToRequestQueue(request); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); preferences = getSharedPreferences( getString(R.string.preferences_file_key), Context.MODE_PRIVATE); Button button = (Button) findViewById(R.id.button_login); final EditText editText = (EditText) findViewById(R.id.edit_text_domain); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onButtonClick(editText); } }); } @Override protected void onPause() { super.onPause(); SharedPreferences.Editor editor = preferences.edit(); editor.putString("domain", domain); editor.putString("clientId", clientId); editor.putString("clientSecret", clientSecret); editor.commit(); } private void onLoginSuccess(String accessToken) { SharedPreferences.Editor editor = preferences.edit(); editor.putString("accessToken", accessToken); editor.apply(); Intent intent = new Intent(this, MainActivity.class); startActivity(intent); finish(); } @Override protected void onResume() { super.onResume(); /* Check if we are resuming during authorization by seeing if the intent contains the * redirect that was given to the server. If so, its response is here! */ Uri uri = getIntent().getData(); String redirectUri = getOauthRedirectUri(); if (uri != null && uri.toString().startsWith(redirectUri)) { // This should either have returned an authorization code or an error. String code = uri.getQueryParameter("code"); String error = uri.getQueryParameter("error"); final TextView errorText = (TextView) findViewById(R.id.text_error); if (code != null) { /* During the redirect roundtrip this Activity usually dies, which wipes out the * instance variables, so they have to be recovered from where they were saved in * SharedPreferences. */ domain = preferences.getString("domain", null); clientId = preferences.getString("clientId", null); clientSecret = preferences.getString("clientSecret", null); /* Since authorization has succeeded, the final step to log in is to exchange * the authorization code for an access token. */ JSONObject parameters = new JSONObject(); try { parameters.put("client_id", clientId); parameters.put("client_secret", clientSecret); parameters.put("redirect_uri", redirectUri); parameters.put("code", code); parameters.put("grant_type", "authorization_code"); } catch (JSONException e) { errorText.setText("Heck."); //TODO: I don't even know how to handle this error state. } String endpoint = getString(R.string.endpoint_token); String url = "https://" + domain + endpoint; JsonObjectRequest request = new JsonObjectRequest( Request.Method.POST, url, parameters, new Response.Listener() { @Override public void onResponse(JSONObject response) { String accessToken = ""; try { accessToken = response.getString("access_token"); } catch(JSONException e) { errorText.setText("Heck."); //TODO: I don't even know how to handle this error state. } onLoginSuccess(accessToken); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { errorText.setText(error.getMessage()); } }); VolleySingleton.getInstance(this).addToRequestQueue(request); } else if (error != null) { /* Authorization failed. Put the error response where the user can read it and they * can try again. */ errorText.setText(error); } else { assert(false); // This case means a junk response was received somehow. errorText.setText("An unidentified authorization error occurred."); } } } }