Commit 102785c9 authored by Roger Barton's avatar Roger Barton
Browse files

Cleaned up auth process, getting user info, logout

Still need to store userinfo between sessions
parent a6c9e8c8
......@@ -24,6 +24,8 @@ dependencies {
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
implementation 'com.android.support:support-v4:27.1.1'
implementation 'com.mcxiaoke.volley:library:1.0.19'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
......
......@@ -14,8 +14,8 @@
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:label=""
android:screenOrientation="portrait"
android:supportsRtl="true"
android:theme="@style/AppThemeLight">
......@@ -29,14 +29,15 @@
</activity>
<activity
android:name=".MainActivity"
android:theme="@style/AppThemeLight"
android:label="">
android:label="@string/app_name"
android:theme="@style/AppThemeLight">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".LoginActivity"
<activity
android:name=".LoginActivity"
android:label="">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
......
......@@ -2,6 +2,7 @@ package ch.amiv.android_app;
import android.content.Intent;
import android.net.Uri;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
......@@ -15,7 +16,7 @@ import java.security.SecureRandom;
public class LoginActivity extends AppCompatActivity {
WebView webView;
private static final String API_OAUTH_URL = "http://192.168.1.105:5000/oauth?response_type=token&client_id=AndroidApp&redirect_uri=http://localhost&state=";
private static final String API_OAUTH_URL = Settings.API_URL + "oauth?response_type=token&client_id=Android+App&redirect_uri=http://localhost:5000&state=";
private String CSRFState = "";
@Override
......@@ -47,10 +48,13 @@ public class LoginActivity extends AppCompatActivity {
{
Settings.SetToken(token, getApplicationContext());
//Notify and update UI
StartMainActivity();
StartMainActivity(true);
}
else
else {
view.loadUrl(GenerateOAuthUrl()); //states do not match so retry login, possible CSRF attack occured
Snackbar.make(view, "Error Occured, Please Retry", Snackbar.LENGTH_LONG).show();
Log.e("login", "OAuth amiv api login: CSRF States do not match, token invalid! Potential CSRF attack occured, will redirect to login retry.");
}
}
......@@ -68,9 +72,11 @@ public class LoginActivity extends AppCompatActivity {
//webView.getSettings().setJavaScriptEnabled(true);
}
private void StartMainActivity() {
private void StartMainActivity(boolean sucess) {
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("login_sucess", sucess);
startActivity(intent);
}
/**
......@@ -87,7 +93,7 @@ public class LoginActivity extends AppCompatActivity {
if (webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
StartMainActivity(false);
}
}
......
......@@ -10,8 +10,10 @@ import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.ListFragment;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.RecyclerView;
import android.util.Base64;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.support.design.widget.NavigationView;
import android.support.v4.view.GravityCompat;
......@@ -19,7 +21,6 @@ import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
......@@ -27,9 +28,26 @@ import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.android.volley.AuthFailureError;
import com.android.volley.NetworkResponse;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.StringRequest;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
public class MainActivity extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener {
NavigationView navigationView;
TextView drawer_title;
TextView drawer_subtitle;
RecyclerView mRecylerView;
RecyclerView.Adapter mRecylcerAdaper;
RecyclerView.LayoutManager mRecyclerLayoutAdapter;
......@@ -51,21 +69,12 @@ public class MainActivity extends AppCompatActivity
}
};
//region Initialisation
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/*FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});*/
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
......@@ -74,20 +83,26 @@ public class MainActivity extends AppCompatActivity
drawer.addDrawerListener(toggle);
toggle.syncState();
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
navigationView = findViewById(R.id.nav_view);
navigationView.setNavigationItemSelectedListener(this);
drawer_title = navigationView.getHeaderView(0).findViewById(R.id.drawer_user_title);
drawer_subtitle = navigationView.getHeaderView(0).findViewById(R.id.drawer_user_subtitle);
new Settings(getApplicationContext());
BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.bottomNav);
BottomNavigationView navigation = findViewById(R.id.bottomNav);
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
//Recyclerview
InitialiseListView();
InitialisePageView();
new Settings(getApplicationContext());
if(Settings.IsLoggedIn() && UserInfo.current == null)
FetchUserData();
else
SetLoginUIDirty();
}
private void InitialiseListView() {
private void InitialisePageView() {
MyAdapter pagerAdapter = new MyAdapter(getSupportFragmentManager());
ViewPager viewPager = findViewById(R.id.viewPager);
viewPager.setAdapter(pagerAdapter);
......@@ -108,21 +123,129 @@ public class MainActivity extends AppCompatActivity
mRecylerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(this, R.anim.layout_anim_falldown));
mRecylerView.setAdapter(mRecylcerAdaper);*/
}
//endregion
//=====Changing Activity====
public void StartSettingsActivity()
@Override
protected void onResume() {
super.onResume();
// If we are returning from the login activity and have had a successfuly login, refresh the user info and login UI
Intent intent = getIntent();
boolean refreshLogin = intent.getBooleanExtra("login_sucess", false);
if(refreshLogin && Settings.IsLoggedIn()){
FetchUserData();
}
}
//region Login
/**
* Will fetch the user from the api if we have an access token. ie Token -> User. Data is stored in the current userinfo (UserInfo.current). Overwrites the current user info if it exists
*/
private void FetchUserData()
{
if(!Settings.IsLoggedIn())
return;
//Do request Token->User
String url = Settings.API_URL + "sessions/" + Settings.GetToken(getApplicationContext()) + "?embedded={\"user\":1}";
StringRequest request = new StringRequest(Request.Method.GET, url,null, null)
{
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) { //Note: the parseNetworkResponse is only called if the response was successful (codes 2xx), else parseNetworkError is called.
if(response != null) {
Log.e("request", "status Code: " + response.statusCode);
try {
JSONObject json = new JSONObject(new String(response.data)).getJSONObject("user");
UserInfo user = new UserInfo(json);
user.SetAsCurrent();
//Update UI in nav drawer
navigationView.post(new Runnable() { //Run updating UI on UI thread
public void run() {
SetLoginUIDirty();
}});
//Log.e("request", json.toString());
} catch (JSONException e) {
e.printStackTrace();
}
}
else
Log.e("request", "Request returned null response.");
return super.parseNetworkResponse(response);
}
@Override
protected VolleyError parseNetworkError(final VolleyError volleyError) { //see comments at parseNetworkResponse()
if(volleyError != null && volleyError.networkResponse != null)
Log.e("request", "status code: " + volleyError.networkResponse.statusCode + "\n" + volleyError.networkResponse.data.toString());
else
Log.e("request", "Request returned null response.");
return super.parseNetworkError(volleyError);
}
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String,String> headers = new HashMap<String, String>();
// Add basic auth with token
String credentials = Settings.GetToken(getApplicationContext()) + ":";
String auth = "Basic " + Base64.encodeToString(credentials.getBytes(), Base64.NO_WRAP);
headers.put("Authorization", auth);
return headers;
}
};
Requests.SendRequest(request, getApplicationContext());
}
/**
* Log the user out, delete token. Will not log the user out of the device with the API as this is used with other amiv services as well
*/
public void LogoutUser()
{
Settings.SetToken("", getApplicationContext());
UserInfo.current = null;
SetLoginUIDirty();
System.gc();//run garbage collector explicitly to clean up user data
}
/**
* Will refresh all login related UI, use this when the user logs in/out
*/
public void SetLoginUIDirty ()
{
if(Settings.IsLoggedIn()) {
if(UserInfo.current == null)
FetchUserData();
else {
navigationView.getMenu().findItem(R.id.nav_login).setTitle("Logout");
drawer_title.setText(UserInfo.current.firstname + " " + UserInfo.current.lastname);
drawer_subtitle.setText(UserInfo.current.email);
}
}
else
{
navigationView.getMenu().findItem(R.id.nav_login).setTitle("Login");
drawer_title.setText("Not Logged In");
drawer_subtitle.setText("");
}
}
//endregion
//=====Changing Activity====
public void StartSettingsActivity() {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
}
public void StartLoginActivity()
{
public void StartLoginActivity() {
Intent intent = new Intent(this, LoginActivity.class);
startActivity(intent);
//Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://192.168.1.105:5000/oauth?response_type=token&client_id=AndroidApp&redirect_uri=http://localhost&state=123"));
//startActivity(browserIntent);
}
//=====TOOLBAR=====
......@@ -169,7 +292,10 @@ public class MainActivity extends AppCompatActivity
int id = item.getItemId();
if (id == R.id.nav_login) {
StartLoginActivity();
if(Settings.IsLoggedIn())
LogoutUser();
else
StartLoginActivity();
} else if (id == R.id.nav_checkin) {
} else if (id == R.id.nav_settings) {
......
package ch.amiv.android_app;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.util.Log;
import com.android.volley.AuthFailureError;
import com.android.volley.NetworkResponse;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public final class Requests {
private static String ON_SUBMIT_PIN_URL_EXT = "/checkpin";
private static String ON_SUBMIT_LEGI_URL_EXT = "/mutate";
private static String GET_DATA_URL_EXT = "/checkin_update_data";
public static RequestQueue requestQueue;
//=======CALLBACK INTERFACES========
/**
* Used for doing callbacks when the memberDB has been updated
*/
public interface OnDataReceivedCallback {
void OnDataReceived(int statusCode, String data);
}
public interface OnCheckPinReceivedCallback { //used for doing callbacks when the memberDB has been updated
void OnStringReceived(boolean validResponse, int statusCode, String data);
}
public interface OnJsonReceivedCallback { //used for doing callbacks when the memberDB has been updated
void OnJsonReceived(int statusCode, JSONObject data);
void OnStringReceived (int statusCode, String data);
}
//====END OF CALLBACKS======
public enum ServerTarget {None, API, Checkin}
/**
* Send a created request with the requestQueue
* @param request A volley request of generic type, eg StringRequest, JsonRequest
*/
public static boolean SendRequest(Request request, Context context){
if(!CheckConnection(context))
return false;
if(requestQueue == null)
requestQueue = Volley.newRequestQueue(context); //Adds the defined post request to the queue to be sent to the server
requestQueue.add(request);
return true;
}
/**
* Will replace illegal characters correctly
* @param url
* @return
*/
public static String EncodeUrl(String url)
{
String encodedUrl = "";
try {
encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return "";
}
return encodedUrl;
}
/**
*
* @param context
* @param callback
* @return If the request was sucessfuly sent, may be false if there is not connction
*/
public static boolean Request(Request.Method requestMethod, final Context context, final ServerTarget serverTarget, final String url_, final HashMap<String, String> params, final HashMap<String, String> header, final HashMap<String, String> body, final OnJsonReceivedCallback callback){
if(!CheckConnection(context))
return false;
//create URL
String url = "";
switch (serverTarget) {
case None:
break;
case API:
url += Settings.API_URL;
break;
case Checkin:
break;
default:
break;
}
url += url_;
String encodedUrl = "";
try {
encodedUrl = URLEncoder.encode(url, java.nio.charset.StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return false;
}
StringRequest request = new StringRequest(requestMethod.hashCode(), encodedUrl,
/*new Response.Listener<String>() { @Override public void onResponse(String response){} },
new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error){} }*/
null, null)
{
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) { //Note: the parseNetworkResponse is only called if the response was successful (codes 2xx), else parseNetworkError is called.
if(response != null)
callback.OnStringReceived(response.statusCode, new String(response.data));
else
callback.OnStringReceived(400, "");
return super.parseNetworkResponse(response);
}
@Override
protected VolleyError parseNetworkError(final VolleyError volleyError) { //see comments at parseNetworkResponse()
if(volleyError != null && volleyError.networkResponse != null)
callback.OnStringReceived(volleyError.networkResponse.statusCode, new String(volleyError.networkResponse.data));
else
callback.OnStringReceived(400, "");
return super.parseNetworkError(volleyError);
}
//==Adding the content==
@Override
protected Map<String, String> getParams() {
if(Settings.IsLoggedIn())
params.put("token", Settings.GetToken(context));
return params;
}
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
return header;
}
@Override
public byte[] getBody() throws AuthFailureError {
StringBuilder tmp = new StringBuilder();
Iterator it = body.entrySet().iterator();
while (it.hasNext()) {
Map.Entry pair = (Map.Entry)it.next();
tmp.append(pair.getKey()).append(":").append(pair.getValue()).append("\n");
it.remove();
}
return tmp.toString().getBytes();
}
};
return SendRequest(request, context);
}
/**
* @return returns true if there is an active internet connection, test this before requesting something from the server
*/
public static boolean CheckConnection(Context context)
{
ConnectivityManager cm = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
if(activeNetwork == null || !activeNetwork.isConnectedOrConnecting())
{
Log.e("postrequest", "No active internet connection");
return false;
}
return true;
}
}
\ No newline at end of file
......@@ -2,6 +2,7 @@ package ch.amiv.android_app;
import android.content.Context;
import android.content.SharedPreferences;
import android.media.session.MediaSession;
import android.util.Log;
import java.net.ConnectException;
......@@ -14,6 +15,8 @@ import java.net.ConnectException;
public class Settings {
public static Settings instance;
public static final String API_URL = "http://10.2.42.121:5000/";
//Vars for saving/reading the url from shared prefs, to allow saving between sessions. For each variable, have a key to access it and a default value
private static SharedPreferences sharedPrefs;
private static final String SHARED_PREFS_KEY = "ch.amiv.android_app";
......@@ -75,6 +78,15 @@ public class Settings {
return sharedPrefs.getString(apiTokenKey, "");
}
/**
* Note: will only check if a token exists. This token may have expired but not have been refreshed/deleted.
* @return True if the user is logged into the api and has an access token.
*/
public static boolean IsLoggedIn(){
String t = sharedPrefs.getString(apiTokenKey, "");
return !t.isEmpty();
}
// Theme
public static void SetIsDarkTheme(boolean value, Context context) {
......
package ch.amiv.android_app;
import org.json.JSONException;
import org.json.JSONObject;
public class UserInfo {
public static UserInfo current;
public String firstname = "";
public String lastname = "";
public String nethz = "";
public String email = "";
public String membership = "";
public String gender = "";
public UserInfo (JSONObject json)
{
try {
if (json.has("firstname"))
firstname = json.getString("firstname");
if (json.has("lastname"))
lastname = json.getString("lastname");
if (json.has("nethz"))
nethz = json.getString("nethz");
if (json.has("email"))
email = json.getString("email");
if (json.has("membership"))
membership = json.getString("membership");
if (json.has("gender"))
gender = json.getString("gender");