Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b6c31cf6 authored by Fahim Salam Chowdhury's avatar Fahim Salam Chowdhury 👽
Browse files

Absract out openId implementation

Current openId login flow is tightly coupled with google auth flow. To
add other openId support, we need to first decouple it. This commit do
just that + it re-factor how openId credentials are shared, now via
gitlab CI variable, which will be pushed in local.properties file on the
build stage.
parent 91a84056
Loading
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -6,6 +6,9 @@ stages:

before_script:
  - echo email.key=$PEPPER >> local.properties
  - echo GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID >> local.properties
  - echo GOOGLE_REDIRECT_URI=$GOOGLE_REDIRECT_URI >> local.properties
  - echo YAHOO_CLIENT_ID=$YAHOO_CLIENT_ID >> local.properties
  - export GRADLE_USER_HOME=$(pwd)/.gradle
  - chmod +x ./gradlew

+11 −5
Original line number Diff line number Diff line
@@ -86,7 +86,7 @@ android {

            shrinkResources true

            buildConfigField "String", "EMAIL_KEY", "\"${retrieveEmailKey()}\""
            buildConfigField "String", "EMAIL_KEY", "\"${retrieveKey("email.key")}\""
        }
    }

@@ -101,8 +101,14 @@ android {
    }

    defaultConfig {
        buildConfigField "String", "GOOGLE_CLIENT_ID", "\"${retrieveKey("GOOGLE_CLIENT_ID")}\""
        buildConfigField "String", "GOOGLE_REDIRECT_URI", "\"${retrieveKey("GOOGLE_REDIRECT_URI")}\""

        buildConfigField "String", "YAHOO_CLIENT_ID", "\"${retrieveKey("YAHOO_CLIENT_ID")}\""

	    manifestPlaceholders = [
                'appAuthRedirectScheme': 'net.openid.appauthdemo'
                'appAuthRedirectScheme':  applicationId,
                "googleAuthRedirectScheme": retrieveKey("GOOGLE_REDIRECT_URI")
        ]
    }
}
@@ -195,13 +201,13 @@ dependencies {
    testImplementation "com.squareup.okhttp3:mockwebserver:${versions.okhttp}"
}

def retrieveEmailKey() {
def retrieveKey(String keyName) {
    Properties properties = new Properties()
    properties.load(project.rootProject.file('local.properties').newDataInputStream())

    String value = properties.getProperty("email.key")
    String value = properties.getProperty(keyName)
    if (value == null) {
        throw new GradleException("email.key property not found in local.properties file")
        throw new GradleException(keyName + " property not found in local.properties file")
    }

    return value
+2 −2
Original line number Diff line number Diff line
@@ -567,8 +567,8 @@
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data android:scheme="net.openid.appauthdemo" />
                <data android:scheme="com.googleusercontent.apps.100496780587-pbiu5eudcjm6cge2phduc6mt8mgbsmsr" />
                <data android:scheme="${appAuthRedirectScheme}" />
                <data android:scheme="${googleAuthRedirectScheme}" />
            </intent-filter>

        </activity>
+52 −163
Original line number Diff line number Diff line
/*
 * Copyright ECORP SAS 2022
 * Copyright MURENA SAS 2022, 2023
 * This program 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
@@ -16,200 +16,97 @@

package at.bitfire.davdroid.authorization;

import android.content.Context;
import android.content.res.Resources;
import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;

import net.openid.appauth.AuthorizationServiceConfiguration;
import net.openid.appauth.AuthorizationServiceConfiguration.RetrieveConfigurationCallback;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import at.bitfire.davdroid.R;
import at.bitfire.davdroid.BuildConfig;

/**
 * An abstraction of identity providers, containing all necessary info for the demo app.
 */
public class IdentityProvider {

    /**
     * Value used to indicate that a configured property is not specified or required.
     */
    public static final int NOT_SPECIFIED = -1;

    public static final IdentityProvider GOOGLE = new IdentityProvider(
            "Google",
            R.string.google_discovery_uri,
            NOT_SPECIFIED, // auth endpoint is discovered
            NOT_SPECIFIED, // token endpoint is discovered
            R.string.google_client_id,
            NOT_SPECIFIED, // client secret is not required for Google
            R.string.google_auth_redirect_uri,
            R.string.google_scope_string,
            R.string.google_name);

    public static final List<IdentityProvider> PROVIDERS = Arrays.asList(
            GOOGLE);

    public static List<IdentityProvider> getEnabledProviders(Context context) {
        ArrayList<IdentityProvider> providers = new ArrayList<>();
        for (IdentityProvider provider : PROVIDERS) {
            provider.readConfiguration(context);
            providers.add(provider);
        }
        return providers;
    }

            "https://accounts.google.com/.well-known/openid-configuration",
            null,
            null,
            BuildConfig.GOOGLE_CLIENT_ID,
            null,
            BuildConfig.GOOGLE_REDIRECT_URI + ":/oauth2redirect",
            "openid profile email https://www.googleapis.com/auth/carddav https://www.googleapis.com/auth/calendar https://mail.google.com/",
            null
    );

    private final Uri mDiscoveryEndpoint;
    private final Uri mAuthEndpoint;
    private final Uri mTokenEndpoint;
    @NonNull
    public final String name;

    @StringRes
    public final int buttonContentDescriptionRes;

    @StringRes
    private final int mDiscoveryEndpointRes;

    @StringRes
    private final int mAuthEndpointRes;

    @StringRes
    private final int mTokenEndpointRes;

    @StringRes
    private final int mClientIdRes;

    @StringRes
    private final int mClientSecretRes;

    @StringRes
    private final int mRedirectUriRes;

    @StringRes
    private final int mScopeRes;

    private boolean mConfigurationRead = false;
    private Uri mDiscoveryEndpoint;
    private Uri mAuthEndpoint;
    private Uri mTokenEndpoint;
    private String mClientId;
    private String mClientSecret;
    private Uri mRedirectUri;
    private String mScope;
    private final String mClientId;
    private final String mClientSecret;
    @NonNull
    private final Uri mRedirectUri;
    private final String mScope;
    private final String mUserInfoEndpoint;

    IdentityProvider(
            @NonNull String name,
            @StringRes int discoveryEndpointRes,
            @StringRes int authEndpointRes,
            @StringRes int tokenEndpointRes,
            @StringRes int clientIdRes,
            @StringRes int clientSecretRes,
            @StringRes int redirectUriRes,
            @StringRes int scopeRes,
            @StringRes int buttonContentDescriptionRes) {
        if (!isSpecified(discoveryEndpointRes)
                && !isSpecified(authEndpointRes)
                && !isSpecified(tokenEndpointRes)) {
            @Nullable String discoveryEndpoint,
            @Nullable String authEndpoint,
            @Nullable String tokenEndpoint,
            @NonNull String clientId,
            @Nullable String clientSecret,
            @NonNull String redirectUri,
            @Nullable String scope,
            @Nullable String userInfoEndpoint) {
        if (discoveryEndpoint == null &&
                (authEndpoint == null || tokenEndpoint == null)) {
            throw new IllegalArgumentException(
                    "the discovery endpoint or the auth and token endpoints must be specified");
        }

        this.name = name;
        this.mDiscoveryEndpointRes = discoveryEndpointRes;
        this.mAuthEndpointRes = authEndpointRes;
        this.mTokenEndpointRes = tokenEndpointRes;
        this.mClientIdRes = checkSpecified(clientIdRes, "clientIdRes");
        this.mClientSecretRes = clientSecretRes;
        this.mRedirectUriRes = checkSpecified(redirectUriRes, "redirectUriRes");
        this.mScopeRes = checkSpecified(scopeRes, "scopeRes");
        this.buttonContentDescriptionRes =
                checkSpecified(buttonContentDescriptionRes, "buttonContentDescriptionRes");
    }

    /**
     * This must be called before any of the getters will function.
     */
    public void readConfiguration(Context context) {
        if (mConfigurationRead) {
            return;
        }

        Resources res = context.getResources();

        mDiscoveryEndpoint = isSpecified(mDiscoveryEndpointRes)
                ? getUriResource(res, mDiscoveryEndpointRes, "discoveryEndpointRes")
                : null;
        mAuthEndpoint = isSpecified(mAuthEndpointRes)
                ? getUriResource(res, mAuthEndpointRes, "authEndpointRes")
                : null;
        mTokenEndpoint = isSpecified(mTokenEndpointRes)
                ? getUriResource(res, mTokenEndpointRes, "tokenEndpointRes")
                : null;
        mClientId = res.getString(mClientIdRes);
        mClientSecret = isSpecified(mClientSecretRes) ? res.getString(mClientSecretRes) : null;
        mRedirectUri = getUriResource(res, mRedirectUriRes, "mRedirectUriRes");
        mScope = res.getString(mScopeRes);

        mConfigurationRead = true;
    }

    private void checkConfigurationRead() {
        if (!mConfigurationRead) {
            throw new IllegalStateException("Configuration not read");
        }
    }

    @Nullable
    public Uri getDiscoveryEndpoint() {
        checkConfigurationRead();
        return mDiscoveryEndpoint;
    }

    @Nullable
    public Uri getAuthEndpoint() {
        checkConfigurationRead();
        return mAuthEndpoint;
    }

    @Nullable
    public Uri getTokenEndpoint() {
        checkConfigurationRead();
        return mTokenEndpoint;
        this.mDiscoveryEndpoint = retrieveUri(discoveryEndpoint);
        this.mAuthEndpoint = retrieveUri(authEndpoint);
        this.mTokenEndpoint = retrieveUri(tokenEndpoint);
        this.mClientId = clientId;
        this.mClientSecret = clientSecret;
        this.mRedirectUri = Objects.requireNonNull(retrieveUri(redirectUri));
        this.mScope = scope;
        this.mUserInfoEndpoint = userInfoEndpoint;
    }

    @NonNull
    public String getClientId() {
        checkConfigurationRead();
        return mClientId;
    }

    @Nullable
    public String getClientSecret() {
        checkConfigurationRead();
        return mClientSecret;
    }

    @NonNull
    public Uri getRedirectUri() {
        checkConfigurationRead();
        return mRedirectUri;
    }

    @NonNull
    public String getScope() {
        checkConfigurationRead();
        return mScope;
    }

    public void retrieveConfig(Context context,
                               RetrieveConfigurationCallback callback) {
        readConfiguration(context);
        if (getDiscoveryEndpoint() != null) {
    @Nullable
    public String getUserInfoEndpoint() {
        return mUserInfoEndpoint;
    }

    public void retrieveConfig(RetrieveConfigurationCallback callback) {
        if (mDiscoveryEndpoint != null) {
            AuthorizationServiceConfiguration.fetchFromUrl(mDiscoveryEndpoint, callback);
        } else {
            AuthorizationServiceConfiguration config =
@@ -218,19 +115,11 @@ public class IdentityProvider {
        }
    }

    private static boolean isSpecified(int value) {
        return value != NOT_SPECIFIED;
    }

    private static int checkSpecified(int value, String valueName) {
        if (value == NOT_SPECIFIED) {
            throw new IllegalArgumentException(valueName + " must be specified");
    @Nullable
    private Uri retrieveUri(@Nullable String value) {
        if (value == null) {
            return null;
        }
        return value;
        return Uri.parse(value);
    }

    private static Uri getUriResource(Resources res, @StringRes int resId, String resName) {
        return Uri.parse(res.getString(resId));
}
}
+79 −0
Original line number Diff line number Diff line
/*
 * Copyright MURENA SAS 2023
 * This program 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.
 *
 * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package at.bitfire.davdroid.ui.setup

import android.os.Bundle
import android.text.Layout
import android.text.SpannableString
import android.text.style.AlignmentSpan
import android.view.View
import at.bitfire.davdroid.R
import at.bitfire.davdroid.authorization.IdentityProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.json.JSONObject

class GoogleAuthFragment : OpenIdAuthenticationBaseFragment(IdentityProvider.GOOGLE) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        handleConfirmationDialog()
    }

    override fun onAuthenticationComplete(userData: JSONObject) {
        val emailKey = "email"

        if (!userData.has(emailKey)) {
            handleLoginFailedToast()
            return
        }

        val email = userData.getString(emailKey)
        if (email.isBlank()) {
            handleLoginFailedToast()
            return
        }

        val baseUrl = "https://apidata.googleusercontent.com/caldav/v2/$email/user"
        proceedNext(email, baseUrl)
    }

    private fun handleConfirmationDialog() {
        if (isAuthFlowComplete()) {
            return
        }

        showConfirmationDialog()
    }

    private fun showConfirmationDialog() {
        val title = SpannableString(getString(R.string.google_alert_title))
        title.setSpan(
            AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
            0,
            title.length,
            0
        )

        MaterialAlertDialogBuilder(requireContext(), R.style.CustomAlertDialogStyle)
            .setTitle(title)
            .setMessage(R.string.google_alert_message)
            .setCancelable(false)
            .setPositiveButton(R.string.ok) { _, _ ->
                startAuthFLow()
            }.show()
    }
}
Loading