Gmail OAuth - proof of concept

This commit is contained in:
M66B 2019-12-20 12:20:50 +01:00
parent 916a966ce4
commit f496a0fa6c
9 changed files with 161 additions and 3 deletions

View File

@ -19,3 +19,4 @@ FairEmail uses:
* [Markwon](https://github.com/noties/Markwon). Copyright 2019 Dimitry Ivanov. [Apache License 2.0](https://github.com/noties/Markwon/blob/master/LICENSE).
* [Color Picker](https://github.com/QuadFlask/colorpicker). Copyright 2014-2017 QuadFlask. [Apache License 2.0](https://github.com/QuadFlask/colorpicker#user-content-license).
* [Bouncy Castle](https://www.bouncycastle.org/). Copyright (c) 2000 - 2019 The Legion of the Bouncy Castle Inc. [MIT License](https://www.bouncycastle.org/licence.html).
* [AppAuth for Android](https://github.com/openid/AppAuth-Android). Copyright 2015 The AppAuth for Android Authors. All Rights Reserved. [Apache License 2.0](https://github.com/openid/AppAuth-Android/blob/master/LICENSE).

View File

@ -214,6 +214,7 @@ dependencies {
def msal_version = "1.0.0"
def bouncycastle_version = "1.64"
def colorpicker_version = "0.0.15"
def appauth_version = "0.7.1"
// https://developer.android.com/jetpack/androidx/releases/
@ -339,4 +340,8 @@ dependencies {
// https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on
implementation "org.bouncycastle:bcpkix-jdk15to18:$bouncycastle_version"
//implementation "org.bouncycastle:bcmail-jdk15to18:$bouncycastle_version"
// https://github.com/openid/AppAuth-Android
// https://mvnrepository.com/artifact/net.openid/appauth?repo=bt-openid
implementation "net.openid:appauth:$appauth_version"
}

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="eu.faircode.email">
<uses-permission android:name="android.permission.INTERNET" />
@ -227,6 +228,22 @@
android:icon="@mipmap/ic_launcher"
android:launchMode="singleTask" />
<activity
android:name="net.openid.appauth.RedirectUriReceiverActivity"
tools:node="replace">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="email.faircode.eu"
android:path="/oauth/"
android:scheme="https" />
</intent-filter>
</activity>
<service
android:name=".ServiceSynchronize"
android:foregroundServiceType="dataSync" />

View File

@ -19,3 +19,4 @@ FairEmail uses:
* [Markwon](https://github.com/noties/Markwon). Copyright 2019 Dimitry Ivanov. [Apache License 2.0](https://github.com/noties/Markwon/blob/master/LICENSE).
* [Color Picker](https://github.com/QuadFlask/colorpicker). Copyright 2014-2017 QuadFlask. [Apache License 2.0](https://github.com/QuadFlask/colorpicker#user-content-license).
* [Bouncy Castle](https://www.bouncycastle.org/). Copyright (c) 2000 - 2019 The Legion of the Bouncy Castle Inc. [MIT License](https://www.bouncycastle.org/licence.html).
* [AppAuth for Android](https://github.com/openid/AppAuth-Android). Copyright 2015 The AppAuth for Android Authors. All Rights Reserved. [Apache License 2.0](https://github.com/openid/AppAuth-Android/blob/master/LICENSE).

View File

@ -73,6 +73,21 @@ import com.microsoft.identity.client.IPublicClientApplication;
import com.microsoft.identity.client.PublicClientApplication;
import com.microsoft.identity.client.exception.MsalException;
import net.openid.appauth.AppAuthConfiguration;
import net.openid.appauth.AuthorizationException;
import net.openid.appauth.AuthorizationRequest;
import net.openid.appauth.AuthorizationResponse;
import net.openid.appauth.AuthorizationService;
import net.openid.appauth.AuthorizationServiceConfiguration;
import net.openid.appauth.ClientAuthentication;
import net.openid.appauth.ClientSecretPost;
import net.openid.appauth.ResponseTypeValues;
import net.openid.appauth.TokenResponse;
import net.openid.appauth.browser.BrowserBlacklist;
import net.openid.appauth.browser.Browsers;
import net.openid.appauth.browser.VersionRange;
import net.openid.appauth.browser.VersionedBrowserMatcher;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.json.JSONArray;
@ -131,8 +146,10 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac
static final int REQUEST_CHOOSE_ACCOUNT = 5;
static final int REQUEST_DONE = 6;
static final int REQUEST_IMPORT_CERTIFICATE = 7;
static final int REQUEST_OAUTH = 8;
static final String ACTION_QUICK_GMAIL = BuildConfig.APPLICATION_ID + ".ACTION_QUICK_GMAIL";
static final String ACTION_QUICK_OAUTH = BuildConfig.APPLICATION_ID + ".ACTION_QUICK_OAUTH";
static final String ACTION_QUICK_OUTLOOK = BuildConfig.APPLICATION_ID + ".ACTION_QUICK_OUTLOOK";
static final String ACTION_QUICK_SETUP = BuildConfig.APPLICATION_ID + ".ACTION_QUICK_SETUP";
static final String ACTION_VIEW_ACCOUNTS = BuildConfig.APPLICATION_ID + ".ACTION_VIEW_ACCOUNTS";
@ -310,6 +327,7 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
IntentFilter iff = new IntentFilter();
iff.addAction(ACTION_QUICK_GMAIL);
iff.addAction(ACTION_QUICK_OAUTH);
iff.addAction(ACTION_QUICK_OUTLOOK);
iff.addAction(ACTION_QUICK_SETUP);
iff.addAction(ACTION_VIEW_ACCOUNTS);
@ -383,6 +401,10 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac
if (resultCode == RESULT_OK && data != null)
handleImportCertificate(data);
break;
case REQUEST_OAUTH:
if (resultCode == RESULT_OK && data != null)
onHandleOAuth(data);
break;
}
} catch (Throwable ex) {
Log.e(ex);
@ -1138,6 +1160,78 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac
fragmentTransaction.commit();
}
private void onOAuth(Intent intent) {
String name = intent.getStringExtra("name");
for (EmailProvider provider : EmailProvider.loadProfiles(this))
if (provider.name.equals(name) && provider.oauth != null) {
AppAuthConfiguration appAuthConfig = new AppAuthConfiguration.Builder()
.setBrowserMatcher(new BrowserBlacklist(
new VersionedBrowserMatcher(
Browsers.SBrowser.PACKAGE_NAME,
Browsers.SBrowser.SIGNATURE_SET,
true,
VersionRange.atMost("5.3")
)))
.build();
AuthorizationService authService = new AuthorizationService(this, appAuthConfig);
AuthorizationRequest authRequest =
new AuthorizationRequest.Builder(
new AuthorizationServiceConfiguration(
Uri.parse(provider.oauth.authorizationEndpoint),
Uri.parse(provider.oauth.tokenEndpoint)),
provider.oauth.clientId,
ResponseTypeValues.CODE,
Uri.parse(provider.oauth.redirectUri))
.setScopes(provider.oauth.scopes)
.setState(name)
.build();
Intent authIntent = authService.getAuthorizationRequestIntent(authRequest);
startActivityForResult(authIntent, REQUEST_OAUTH);
return;
}
Log.unexpectedError(getSupportFragmentManager(),
new IllegalArgumentException("Unknown provider=" + name));
}
private void onHandleOAuth(Intent data) {
AuthorizationResponse auth = AuthorizationResponse.fromIntent(data);
if (auth == null) {
AuthorizationException ex = AuthorizationException.fromIntent(data);
Log.unexpectedError(getSupportFragmentManager(), ex);
return;
}
for (EmailProvider provider : EmailProvider.loadProfiles(this))
if (provider.name.equals(auth.state)) {
AuthorizationService authService = new AuthorizationService(this);
ClientAuthentication clientAuth = new ClientSecretPost(provider.oauth.clientSecret);
authService.performTokenRequest(
auth.createTokenExchangeRequest(),
clientAuth,
new AuthorizationService.TokenResponseCallback() {
@Override
public void onTokenRequestCompleted(TokenResponse access, AuthorizationException ex) {
if (access == null) {
Log.unexpectedError(getSupportFragmentManager(), ex);
return;
}
// access.accessToken
}
});
return;
}
Log.unexpectedError(getSupportFragmentManager(),
new IllegalArgumentException("Unknown state=" + auth.state));
}
private void onOutlook(Intent intent) {
PublicClientApplication.createMultipleAccountPublicClientApplication(
this,
@ -1484,6 +1578,8 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac
String action = intent.getAction();
if (ACTION_QUICK_GMAIL.equals(action))
onGmail(intent);
else if (ACTION_QUICK_OAUTH.equals(action))
onOAuth(intent);
else if (ACTION_QUICK_OUTLOOK.equals(action))
onOutlook(intent);
else if (ACTION_QUICK_SETUP.equals(action))

View File

@ -67,6 +67,7 @@ public class EmailProvider {
public String link;
public Server imap = new Server();
public Server smtp = new Server();
public OAuth oauth;
public UserType user = UserType.EMAIL;
public StringBuilder documentation; // html
@ -129,6 +130,14 @@ public class EmailProvider {
provider.smtp.host = xml.getAttributeValue(null, "host");
provider.smtp.port = xml.getAttributeIntValue(null, "port", 0);
provider.smtp.starttls = xml.getAttributeBooleanValue(null, "starttls", false);
} else if ("oauth".equals(name)) {
provider.oauth = new OAuth();
provider.oauth.clientId = xml.getAttributeValue(null, "clientId");
provider.oauth.clientSecret = xml.getAttributeValue(null, "clientSecret");
provider.oauth.scopes = xml.getAttributeValue(null, "scopes").split(",");
provider.oauth.authorizationEndpoint = xml.getAttributeValue(null, "authorizationEndpoint");
provider.oauth.tokenEndpoint = xml.getAttributeValue(null, "tokenEndpoint");
provider.oauth.redirectUri = xml.getAttributeValue(null, "redirectUri");
} else
throw new IllegalAccessException(name);
} else if (eventType == XmlPullParser.END_TAG) {
@ -650,4 +659,13 @@ public class EmailProvider {
return host + ":" + port;
}
}
public static class OAuth {
String clientId;
String clientSecret;
String[] scopes;
String authorizationEndpoint;
String tokenEndpoint;
String redirectUri;
}
}

View File

@ -167,9 +167,16 @@ public class FragmentSetup extends FragmentBase {
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(getContext(), getViewLifecycleOwner(), btnQuick);
popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_gmail, 1, R.string.title_setup_gmail);
//popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_outlook, 2, R.string.title_setup_outlook);
popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_activesync, 3, R.string.title_setup_activesync);
popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_other, 4, R.string.title_setup_other);
// Android 5 Lollipop does not support app links
if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_gmail_oauth, 2, R.string.title_setup_gmail_oauth);
if (BuildConfig.DEBUG)
popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_outlook, 3, R.string.title_setup_outlook);
popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_activesync, 4, R.string.title_setup_activesync);
popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_other, 5, R.string.title_setup_other);
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
@ -182,6 +189,9 @@ public class FragmentSetup extends FragmentBase {
else
ToastEx.makeText(getContext(), R.string.title_setup_gmail_support, Toast.LENGTH_LONG).show();
return true;
case R.string.title_setup_gmail_oauth:
lbm.sendBroadcast(new Intent(ActivitySetup.ACTION_QUICK_OAUTH).putExtra("name", "Gmail"));
return true;
case R.string.title_setup_outlook:
lbm.sendBroadcast(new Intent(ActivitySetup.ACTION_QUICK_OUTLOOK));
return true;

View File

@ -140,6 +140,7 @@
<string name="title_setup_wizard">Wizard</string>
<string name="title_setup_wizard_remark">Go \'back\' to go to the inbox</string>
<string name="title_setup_gmail" translatable="false">Gmail</string>
<string name="title_setup_gmail_oauth" translatable="false">Gmail OAuth</string>
<string name="title_setup_outlook" translatable="false">Outlook</string>
<string name="title_setup_activesync" translatable="false">Exchange ActiveSync</string>
<string name="title_setup_other">Other provider</string>

View File

@ -14,6 +14,15 @@
host="smtp.gmail.com"
port="465"
starttls="false" />
<oauth
authorizationEndpoint="https://accounts.google.com/o/oauth2/v2/auth"
clientId="803253368361-574lor1js3csqif9nogkhk5m7688af3c.apps.googleusercontent.com"
clientSecret="9iyiDx1LEfpg3fpH6DqzoIcG"
redirectUri="https://email.faircode.eu/oauth/"
scopes="https://mail.google.com/"
tokenEndpoint="https://oauth2.googleapis.com/token" />
<!-- https://email.faircode.eu/.well-known/assetlinks.json -->
<!-- /opt/android-studio/jre/bin/keytool -keystore ~/.android/debug.keystore -list -v -->
</provider>
<provider
name="Outlook/Office365"