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

Commit 77b9e84d authored by Nihar Thakkar's avatar Nihar Thakkar
Browse files

Implement OAuth authorisation and authentication for eelo accounts.

parent 3091b3c1
Loading
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -66,7 +66,7 @@ android {

    defaultConfig {
        manifestPlaceholders = [
                'appAuthRedirectScheme': 'com.googleusercontent.apps.628867657910-7ade6gut5rhabdgjq6k4rln9i1u9ppca'
                'appAuthRedirectScheme': 'net.openid.appauthdemo'
        ]
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
+1 −20
Original line number Diff line number Diff line
@@ -254,29 +254,10 @@
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data android:scheme="net.openid.appauth.demo" />
                <data android:scheme="net.openid.appauthdemo" />
                <data android:scheme="com.googleusercontent.apps.628867657910-7ade6gut5rhabdgjq6k4rln9i1u9ppca" />
            </intent-filter>

            <!--
                for up-to-date Chrome browsers we can intercept the OAuth2 callback directly, if a secure assertion
                is found on the site matching this app:
                https://appauth.demo-app.io/.well-known/assetlinks.json
                The source for this file can also be found in
                web/.well-known/assetlinks.json
            -->
            <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="appauth.demo-app.io"
                    android:path="/oauth2redirect"
                    android:scheme="https" />
            </intent-filter>

        </activity>

    </application>
+13 −1
Original line number Diff line number Diff line
@@ -52,8 +52,20 @@ public class IdentityProvider
            R.string.google_scope_string,
            R.string.google_name);

    public static final IdentityProvider EELO = new IdentityProvider(
            "eelo",
            R.string.eelo_discovery_uri,
            NOT_SPECIFIED, // auth endpoint is discovered
            NOT_SPECIFIED, // token endpoint is discovered
            R.string.eelo_client_id,
            NOT_SPECIFIED, // client secret is not required for Google
            R.string.eelo_auth_redirect_uri,
            R.string.eelo_scope_string,
            R.string.eelo_name);

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

    public static List<IdentityProvider> getEnabledProviders(Context context)
    {
+392 −0
Original line number Diff line number Diff line
package at.bitfire.davdroid.ui.setup


import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.*
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import at.bitfire.dav4android.Constants

import at.bitfire.davdroid.authorization.IdentityProvider

import at.bitfire.davdroid.R
import kotlinx.android.synthetic.main.login_credentials_fragment.view.*
import net.openid.appauth.*
import org.json.JSONException
import org.json.JSONObject
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.net.*
import java.util.HashMap
import java.util.logging.Level
import android.net.ConnectivityManager
import android.widget.Toast


class EeloAuthenticatorFragment : Fragment(), AuthorizationService.TokenResponseCallback {

    private val extraAuthServiceDiscovery = "authServiceDiscovery"
    private val extraClientSecret = "clientSecret"

    private var authState: AuthState? = null
    private var authorizationService: AuthorizationService? = null

    private val bufferSize = 1024
    private var userInfoJson: JSONObject? = null

    private fun isNetworkAvailable(): Boolean {
        val connectivityManager = activity!!.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val activeNetworkInfo = connectivityManager.activeNetworkInfo
        return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_eelo_authenticator, container, false)

        if (!isNetworkAvailable()) {
            Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
            activity!!.finish()
        }

        // Initialise the authorization service
        authorizationService = AuthorizationService(context!!)

        activity?.intent?.let {
            if (!it.getBooleanExtra(LoginActivity.ACCOUNT_PROVIDER_EELO_AUTH_COMPLETE, false)) {
                // Get all the account providers
                val providers = IdentityProvider.getEnabledProviders(context)

                // Iterate over the account providers
                for (idp in providers) {
                    val retrieveCallback = AuthorizationServiceConfiguration.RetrieveConfigurationCallback { serviceConfiguration, ex ->
                        if (ex == null && serviceConfiguration != null) {
                            makeAuthRequest(serviceConfiguration, idp)
                        }
                        else {
                            Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show()
                            activity!!.finish()
                        }
                    }

                    if (idp.name == getString(R.string.eelo_name)) {
                        // Get configurations for the eelo account provider
                        idp.retrieveConfig(context, retrieveCallback)
                    }
                }
            }
            else {
                val response = AuthorizationResponse.fromIntent(activity!!.intent)
                val ex = AuthorizationException.fromIntent(activity!!.intent)
                authState = AuthState(response, ex)

                if (response != null) {
                    exchangeAuthorizationCode(response)
                }
                else {
                    Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show()
                    activity!!.finish()
                }
            }
        }

        return view
    }

    private fun makeAuthRequest(
            serviceConfig: AuthorizationServiceConfiguration,
            idp: IdentityProvider) {
        if (!isNetworkAvailable()) {
            Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
            activity!!.finish()
        }

        val authRequest = AuthorizationRequest.Builder(
                serviceConfig,
                idp.clientId,
                ResponseTypeValues.CODE,
                idp.redirectUri)
                .setScope(idp.scope)
                .build()

        authorizationService?.performAuthorizationRequest(
                authRequest,
                createPostAuthorizationIntent(
                        context!!,
                        authRequest,
                        serviceConfig.discoveryDoc,
                        idp.clientSecret),
                authorizationService?.createCustomTabsIntentBuilder()!!
                        .build())

        requireActivity().setResult(Activity.RESULT_OK)
        requireActivity().finish()
    }

    private fun createPostAuthorizationIntent(
            context: Context,
            request: AuthorizationRequest,
            discoveryDoc: AuthorizationServiceDiscovery?,
            clientSecret: String?): PendingIntent {
        val intent = Intent(context, LoginActivity::class.java)

        if (discoveryDoc != null) {
            intent.putExtra(extraAuthServiceDiscovery, discoveryDoc.docJson.toString())
        }

        if (clientSecret != null) {
            intent.putExtra(extraClientSecret, clientSecret)
        }

        intent.putExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, LoginActivity.ACCOUNT_PROVIDER_EELO)
        intent.putExtra(LoginActivity.ACCOUNT_PROVIDER_EELO_AUTH_COMPLETE, true)

        return PendingIntent.getActivity(context, request.hashCode(), intent, 0)
    }

    private fun exchangeAuthorizationCode(authorizationResponse: AuthorizationResponse) {
        if (!isNetworkAvailable()) {
            Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
            activity!!.finish()
        }

        val additionalParams = HashMap<String, String?>()
        if (getClientSecretFromIntent(activity!!.intent) != null) {
            additionalParams["client_secret"] = getClientSecretFromIntent(activity!!.intent)
        }
        performTokenRequest(authorizationResponse.createTokenExchangeRequest(additionalParams))
    }

    private fun getClientSecretFromIntent(intent: Intent): String? {
        return if (!intent.hasExtra(extraClientSecret)) {
            null
        }
        else intent.getStringExtra(extraClientSecret)
    }


    private fun performTokenRequest(request: TokenRequest) {
        if (!isNetworkAvailable()) {
            Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
            activity!!.finish()
        }

        authorizationService?.performTokenRequest(
                request, this)
    }

    override fun onTokenRequestCompleted(response: TokenResponse?, ex: AuthorizationException?) {
        authState?.update(response, ex)

        // TODO Get the userId for future requests
        //getAccountInfo()
    }

    private fun getAccountInfo() {
        if (!isNetworkAvailable()) {
            Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
            activity!!.finish()
        }

        val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent)

        if (!authState!!.isAuthorized
                || discoveryDoc == null
                || discoveryDoc.userinfoEndpoint == null) {
            Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show()
            activity!!.finish()
        }
        else {
            object : AsyncTask<Void, Void, Void>() {
                override fun doInBackground(vararg params: Void): Void? {
                    if (fetchUserInfo()) {
                        Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show()
                        activity!!.finish()
                    }
                    return null
                }
            }.execute()
        }
    }

    private fun getDiscoveryDocFromIntent(intent: Intent): AuthorizationServiceDiscovery? {
        if (!intent.hasExtra(extraAuthServiceDiscovery)) {
            return null
        }
        val discoveryJson = intent.getStringExtra(extraAuthServiceDiscovery)
        try {
            return AuthorizationServiceDiscovery(JSONObject(discoveryJson))
        }
        catch (ex: JSONException) {
            throw IllegalStateException("Malformed JSON in discovery doc")
        }
        catch (ex: AuthorizationServiceDiscovery.MissingArgumentException) {
            throw IllegalStateException("Malformed JSON in discovery doc")
        }

    }

    private fun fetchUserInfo(): Boolean {
        var error = false

        if (authState!!.authorizationServiceConfiguration == null) {
            return true
        }

        authState!!.performActionWithFreshTokens(authorizationService!!, AuthState.AuthStateAction { accessToken, _, ex ->
            if (ex != null) {
                error = true
                return@AuthStateAction
            }

            val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent)
                    ?: throw IllegalStateException("no available discovery doc")

            val userInfoEndpoint: URL
            try {
                userInfoEndpoint = URL(discoveryDoc.userinfoEndpoint!!.toString())
            }
            catch (urlEx: MalformedURLException) {
                error = true
                return@AuthStateAction
            }

            var userInfoResponse: InputStream? = null
            try {
                val conn = userInfoEndpoint.openConnection() as HttpURLConnection
                conn.setRequestProperty("Authorization", "Bearer " + accessToken!!)
                conn.instanceFollowRedirects = false
                userInfoResponse = conn.inputStream
                val response = readStream(userInfoResponse)
                updateUserInfo(JSONObject(response))
            }
            catch (ioEx: IOException) {
                error = true
            }
            catch (jsonEx: JSONException) {
                error = true
            }
            finally {
                if (userInfoResponse != null) {
                    try {
                        userInfoResponse.close()
                    }
                    catch (ioEx: IOException) {
                        error = true
                    }

                }
            }
        })

        return error
    }

    @Throws(IOException::class)
    private fun readStream(stream: InputStream?): String {
        val br = BufferedReader(InputStreamReader(stream!!))
        val buffer = CharArray(bufferSize)
        val sb = StringBuilder()
        var readCount = br.read(buffer)
        while (readCount != -1) {
            sb.append(buffer, 0, readCount)
            readCount = br.read(buffer)
        }
        return sb.toString()
    }

    private fun updateUserInfo(jsonObject: JSONObject) {
        Handler(Looper.getMainLooper()).post {
            userInfoJson = jsonObject
            onAccountInfoGotten()
        }
    }

    private fun onAccountInfoGotten() {
        if (!isNetworkAvailable()) {
            Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
            activity!!.finish()
        }

        if (userInfoJson != null) {
            try {
                var emailAddress = ""
                if (userInfoJson!!.has("email")) {
                    emailAddress = userInfoJson!!.getString("email")
                }

                validateLoginData(emailAddress, authState!!)?.let { info ->
                    DetectConfigurationFragment.newInstance(info).show(fragmentManager, null)
                }
            }
            catch (ex: JSONException) {
                Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show()
                activity!!.finish()
            }

        }
        else {
            Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show()
            activity!!.finish()
        }
    }

    private fun validateLoginData(emailAddress: String, authState: AuthState): LoginInfo? {
        val baseUrl = Uri.parse("https://apidata.googleusercontent.com/caldav/v2/$emailAddress/events")
        val uri = validateBaseUrl(baseUrl, false, { message ->
            view!!.urlpwd_base_url.error = message
        })

        return if (uri != null)
            LoginInfo(uri, emailAddress, null, authState, null)
        else
            null
    }

    private fun validateBaseUrl(baseUrl: Uri, httpsRequired: Boolean, reportError: (String) -> Unit): URI? {
        var uri: URI? = null
        val scheme = baseUrl.scheme
        if ((!httpsRequired && scheme.equals("http", true)) || scheme.equals("https", true)) {
            var host = baseUrl.host
            if (host.isNullOrBlank())
                reportError(getString(R.string.login_url_host_name_required))
            else
                try {
                    host = IDN.toASCII(host)
                }
                catch (e: IllegalArgumentException) {
                    Constants.log.log(Level.WARNING, "Host name not conforming to RFC 3490", e)
                }

            val path = baseUrl.encodedPath
            val port = baseUrl.port
            try {
                uri = URI(baseUrl.scheme, null, host, port, path, null, null)
            }
            catch (e: URISyntaxException) {
                reportError(e.localizedMessage)
            }
        }
        else
            reportError(getString(if (httpsRequired)
                R.string.login_url_must_be_https
            else
                R.string.login_url_must_be_http_or_https))
        return uri
    }

    override fun onDestroy() {
        super.onDestroy()
        authorizationService?.dispose()
    }


}
+2 −3
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ class LoginActivity : AppCompatActivity() {
        const val ACCOUNT_PROVIDER_EELO = "eelo"
        const val ACCOUNT_PROVIDER_GOOGLE = "google"
        const val ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE = "google_auth_complete"
        const val ACCOUNT_PROVIDER_EELO_AUTH_COMPLETE = "eelo_auth_complete"
    }


@@ -54,10 +55,8 @@ class LoginActivity : AppCompatActivity() {
        if (savedInstanceState == null) {
            when (intent.getStringExtra(SETUP_ACCOUNT_PROVIDER_TYPE)) {
                ACCOUNT_PROVIDER_EELO -> {
                    // Set the eelo Contacts and Calendar service URL
                    intent.putExtra(EXTRA_URL, "https://drive.eelo.io")
                    supportFragmentManager.beginTransaction()
                            .replace(android.R.id.content, DefaultLoginCredentialsFragment())
                            .replace(android.R.id.content, EeloAuthenticatorFragment())
                            .commit()
                }
                ACCOUNT_PROVIDER_GOOGLE -> {
Loading