Android Open Source - dropbox-android-sdk Auth Activity






From Project

Back to project page dropbox-android-sdk.

License

The source code is released under:

Copyright (c) 2009-2011 Dropbox Inc., http://www.dropbox.com/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "So...

If you think the Android project dropbox-android-sdk listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package com.dropbox.client2.android;
//from   w  w w .ja va 2 s  .  com
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.List;
import java.util.Locale;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;

import com.dropbox.client2.DropboxAPI;
import com.dropbox.client2.RESTUtility;

//Note: This class's code is duplicated between Core SDK and Sync SDK.  For now,
//it has to be manually copied, but the code is set up so that it can be used in both
//places, with only a few import changes above.  If you're making changes here, you
//should consider if the other side needs them.  Don't break compatibility if you
//don't have to.  This is a hack we should get away from eventually.

/**
 * This activity is used internally for authentication, but must be exposed both
 * so that Android can launch it and for backwards compatibility.
 */
public class AuthActivity extends Activity {
    private static final String TAG = AuthActivity.class.getName();

    /**
     * The extra that goes in an intent to provide your consumer key for
     * Dropbox authentication. You won't ever have to use this.
     */
    public static final String EXTRA_CONSUMER_KEY = "CONSUMER_KEY";

    /**
     * The extra that goes in an intent when returning from Dropbox auth to
     * provide the user's access token, if auth succeeded. You won't ever have
     * to use this.
     */
    public static final String EXTRA_ACCESS_TOKEN = "ACCESS_TOKEN";

    /**
     * The extra that goes in an intent when returning from Dropbox auth to
     * provide the user's access token secret, if auth succeeded. You won't
     * ever have to use this.
     */
    public static final String EXTRA_ACCESS_SECRET = "ACCESS_SECRET";

    /**
     * The extra that goes in an intent when returning from Dropbox auth to
     * provide the user's Dropbox UID, if auth succeeded. You won't ever have
     * to use this.
     */
    public static final String EXTRA_UID = "UID";

    /**
     * Used for internal authentication. You won't ever have to use this.
     */
    public static final String EXTRA_CONSUMER_SIG = "CONSUMER_SIG";

    /**
     * Used for internal authentication. You won't ever have to use this.
     */
    public static final String EXTRA_CALLING_PACKAGE = "CALLING_PACKAGE";

    /**
     * Used for internal authentication. You won't ever have to use this.
     */
    public static final String EXTRA_CALLING_CLASS = "CALLING_CLASS";

    /**
     * Used for internal authentication. You won't ever have to use this.
     */
    public static final String EXTRA_AUTH_STATE = "AUTH_STATE";

    /**
     * The Android action which the official Dropbox app will accept to
     * authenticate a user. You won't ever have to use this.
     */
    public static final String ACTION_AUTHENTICATE_V1 = "com.dropbox.android.AUTHENTICATE_V1";

    /**
     * The Android action which the official Dropbox app will accept to
     * authenticate a user. You won't ever have to use this.
     */
    public static final String ACTION_AUTHENTICATE_V2 = "com.dropbox.android.AUTHENTICATE_V2";

    /**
     * The version of the API for the web-auth callback with token (not the initial auth request).
     */
    public static final int AUTH_VERSION = 1;

    /**
     * The path for a successful callback with token (not the initial auth request).
     */
    public static final String AUTH_PATH_CONNECT = "/connect";

    private static final String DEFAULT_WEB_HOST = "www.dropbox.com";

    // saved instance state keys
    private static final String SIS_KEY_AUTH_STATE_NONCE = "SIS_KEY_AUTH_STATE_NONCE";

    /**
     * Provider of the local security needs of an AuthActivity.
     *
     * <p>
     * You shouldn't need to use this class directly in your app.  Instead,
     * simply configure {@code java.security}'s providers to match your preferences.
     * </p>
     */
    public interface SecurityProvider {
        /**
         * Gets a SecureRandom implementation for use during authentication.
         */
        SecureRandom getSecureRandom();
    }

    // Class-level state used to replace the default SecureRandom implementation
    // if desired.
    private static SecurityProvider sSecurityProvider = new SecurityProvider() {
        @Override
        public SecureRandom getSecureRandom() {
            return new SecureRandom();
        }
    };
    private static final Object sSecurityProviderLock = new Object();

    /** Used internally. */
    public static Intent result = null;

    // Temporary storage for parameters before Activity is created
    private static String sAppKey;
    private static String sAppSecret;
    private static String sWebHost = DEFAULT_WEB_HOST;
    private static String sApiType;

    // These instance variables need not be stored in savedInstanceState as onNewIntent()
    // does not read them.
    private String mAppKey;
    private String mAppSecret;
    private String mWebHost;
    private String mApiType;

    // Stored in savedInstanceState to track an ongoing auth attempt, which
    // must include a locally-generated nonce in the response.
    private String mAuthStateNonce = null;

    /**
     * Set static authentication parameters
     */
    static void setAuthParams(String appKey, String appSecret) {
        setAuthParams(appKey, appSecret, null, null);
    }

    /**
     * Set static authentication parameters
     */
    static void setAuthParams(String appKey, String appSecret, String webHost, String apiType) {
        sAppKey = appKey;
        sAppSecret = appSecret;
        sWebHost = (webHost != null) ? webHost : DEFAULT_WEB_HOST;
        sApiType = apiType;
    }

    /**
     * Create an intent which can be sent to this activity to start OAuth 1 authentication.
     *
     * @param context the source context
     * @param appKey the consumer key for the app
     * @param appSecret the consumer secret for the app
     * @param webHost the host to use for web authentication, or null for the default
     * @param apiType an identifier for the type of API being supported, or null for
     *  the default
     *
     * @return a newly created intent.
     *
     * @deprecated Use {@link #makeOAuth2Intent}
     */
    public static Intent makeIntent(Context context, String appKey, String appSecret,
                                    String webHost, String apiType) {
        if (appKey == null) throw new IllegalArgumentException("'appKey' can't be null");
        if (appSecret == null) throw new IllegalArgumentException("'appSecret' can't be null");
        setAuthParams(appKey, appSecret, webHost, apiType);
        return new Intent(context, AuthActivity.class);
    }

    /**
     * Create an intent which can be sent to this activity to start OAuth 2 authentication.
     *
     * @param context the source context
     * @param appKey the consumer key for the app
     * @param webHost the host to use for web authentication, or null for the default
     * @param apiType an identifier for the type of API being supported, or null for
     *  the default
     *
     * @return a newly created intent.
     */
    public static Intent makeOAuth2Intent(Context context, String appKey, String webHost,
                                          String apiType) {
        if (appKey == null) throw new IllegalArgumentException("'appKey' can't be null");
        setAuthParams(appKey, null, webHost, apiType);
        return new Intent(context, AuthActivity.class);
    }

    /**
     * Check's the current app's manifest setup for authentication.
     * If the manifest is incorrect, an exception will be thrown.
     * If another app on the device is conflicting with this one,
     * the user will (optionally) be alerted and false will be returned.
     *
     * @param context the app context
     * @param appKey the consumer key for the app
     * @param alertUser whether to alert the user for the case where
     *  multiple apps are conflicting.
     *
     * @return {@code true} if this app is properly set up for authentication.
     */
    public static boolean checkAppBeforeAuth(Context context, String appKey, boolean alertUser) {
        // Check if the app has set up its manifest properly.
        Intent testIntent = new Intent(Intent.ACTION_VIEW);
        String scheme = "db-" +appKey;
        String uri = scheme + "://" + AUTH_VERSION + AUTH_PATH_CONNECT;
        testIntent.setData(Uri.parse(uri));
        PackageManager pm = context.getPackageManager();
        List<ResolveInfo> activities = pm.queryIntentActivities(testIntent, 0);

        if (null == activities || 0 == activities.size()) {
            throw new IllegalStateException("URI scheme in your app's " +
                    "manifest is not set up correctly. You should have a " +
                    AuthActivity.class.getName() + " with the " +
                    "scheme: " + scheme);
        } else if (activities.size() > 1) {
            if (alertUser) {
                AlertDialog.Builder builder = new AlertDialog.Builder(context);
                builder.setTitle("Security alert");
                builder.setMessage("Another app on your phone may be trying to " +
                        "pose as the app you are currently using. The malicious " +
                        "app can't access your account, but linking to Dropbox " +
                        "has been disabled as a precaution. Please contact " +
                        "support@dropbox.com.");
                builder.setPositiveButton("OK", new OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                });
                builder.show();
            } else {
                Log.w(TAG, "There are multiple apps registered for the AuthActivity " +
                        "URI scheme (" + scheme + ").  Another app may be trying to " +
                        " impersonate this app, so authentication will be disabled.");
            }
            return false;
        } else {
            // Just one activity registered for the URI scheme. Now make sure
            // it's within the same package so when we return from web auth
            // we're going back to this app and not some other app.
            ResolveInfo resolveInfo = activities.get(0);
            if (null == resolveInfo || null == resolveInfo.activityInfo
                    || !context.getPackageName().equals(resolveInfo.activityInfo.packageName)) {
                throw new IllegalStateException("There must be a " +
                        AuthActivity.class.getName() + " within your app's package " +
                        "registered for your URI scheme (" + scheme + "). However, " +
                        "it appears that an activity in a different package is " +
                        "registered for that scheme instead. If you have " +
                        "multiple apps that all want to use the same access" +
                        "token pair, designate one of them to do " +
                        "authentication and have the other apps launch it " +
                        "and then retrieve the token pair from it.");
            }
        }

        return true;
    }

    /**
     * Sets the SecurityProvider interface to use for all AuthActivity instances.
     * If set to null (or never set at all), default {@code java.security} providers
     * will be used instead.
     *
     * <p>
     * You shouldn't need to use this method directly in your app.  Instead,
     * simply configure {@code java.security}'s providers to match your preferences.
     * </p>
     *
     * @param prov the new {@code SecurityProvider} interface.
     */
    public static void setSecurityProvider(SecurityProvider prov) {
        synchronized (sSecurityProviderLock) {
            sSecurityProvider = prov;
        }
    }

    private static SecurityProvider getSecurityProvider() {
        synchronized (sSecurityProviderLock) {
            return sSecurityProvider;
        }
    }

    private static SecureRandom getSecureRandom() {
        SecurityProvider prov = getSecurityProvider();
        if (null != prov) {
            return prov.getSecureRandom();
        }
        return new SecureRandom();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        if (savedInstanceState == null) {
            result = null;
            mAuthStateNonce = null;
            mAppKey = sAppKey;
            mAppSecret = sAppSecret;
            mWebHost = sWebHost;
            mApiType = sApiType;
        } else {
            mAuthStateNonce = savedInstanceState.getString(SIS_KEY_AUTH_STATE_NONCE);
        }

        setAuthParams(null, null);
        setTheme(android.R.style.Theme_Translucent_NoTitleBar);

        super.onCreate(savedInstanceState);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString(SIS_KEY_AUTH_STATE_NONCE, mAuthStateNonce);
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (isFinishing()) {
            return;
        }

        if (mAuthStateNonce != null || mAppKey == null) {
            // We somehow returned to this activity without being forwarded
            // here by the official app. Most likely caused by improper setup,
            // but could have other reasons if Android is acting up and killing
            // activities used in our process.
            authFinished(null);
            return;
        }

        result = null;

        // Random entropy passed through auth makes sure we don't accept a
        // response which didn't come from our request.  Each random
        // value is only ever used once.
        String state = createStateNonce();

        // Create intent to auth with official app.
        Intent officialIntent = new Intent();
        officialIntent.setPackage("com.dropbox.android");
        officialIntent.setAction(ACTION_AUTHENTICATE_V2);
        officialIntent.putExtra(EXTRA_CONSUMER_KEY, mAppKey);
        officialIntent.putExtra(EXTRA_CONSUMER_SIG, getConsumerSig());
        officialIntent.putExtra(EXTRA_CALLING_PACKAGE, getPackageName());
        officialIntent.putExtra(EXTRA_CALLING_CLASS, getClass().getName());
        officialIntent.putExtra(EXTRA_AUTH_STATE, state);

        // Auth with official app, or fall back to web.
        if (hasDropboxApp(officialIntent)) {
            startActivity(officialIntent);
        } else {
            startWebAuth(state);
        }

        // Save state that indicates we started a request, only after
        // we started one successfully.
        mAuthStateNonce = state;
    }

    @Override
    protected void onNewIntent(Intent intent) {
        // Reject attempt to finish authentication if we never started (nonce=null)
        if (null == mAuthStateNonce) {
            authFinished(null);
            return;
        }

        String token = null, secret = null, uid = null, state = null;

        if (intent.hasExtra(EXTRA_ACCESS_TOKEN)) {
            // Dropbox app auth.
            token = intent.getStringExtra(EXTRA_ACCESS_TOKEN);
            secret = intent.getStringExtra(EXTRA_ACCESS_SECRET);
            uid = intent.getStringExtra(EXTRA_UID);
            state = intent.getStringExtra(EXTRA_AUTH_STATE);
        } else {
            // Web auth.
            Uri uri = intent.getData();
            if (uri != null) {
                String path = uri.getPath();
                if (AUTH_PATH_CONNECT.equals(path)) {
                    try {
                        token = uri.getQueryParameter("oauth_token");
                        secret = uri.getQueryParameter("oauth_token_secret");
                        uid = uri.getQueryParameter("uid");
                        state = uri.getQueryParameter("state");
                    } catch (UnsupportedOperationException e) {}
                }
            }
        }

        Intent newResult;
        if (token != null && !token.equals("") &&
                (secret == null || !secret.equals("")) &&
                uid != null && !uid.equals("") &&
                state != null && !state.equals("")) {
            // Reject attempt to link if the nonce in the auth state doesn't match,
            // or if we never asked for auth at all.
            if (!mAuthStateNonce.equals(state)) {
                authFinished(null);
                return;
            }

            // Successful auth.
            newResult = new Intent();
            newResult.putExtra(EXTRA_ACCESS_TOKEN, token);
            newResult.putExtra(EXTRA_ACCESS_SECRET, secret);
            newResult.putExtra(EXTRA_UID, uid);
        } else {
            // Unsuccessful auth, or missing required parameters.
            newResult = null;
        }

        authFinished(newResult);
    }

    private void authFinished(Intent authResult) {
        result = authResult;
        mAuthStateNonce = null;
        finish();
    }

    private String getConsumerSig() {
        if (mAppSecret == null) return "";  // We don't use an app secret for OAuth 2.

        MessageDigest m;
        try {
            m = MessageDigest.getInstance("SHA-1");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        m.update(mAppSecret.getBytes(), 0, mAppSecret.length());
        BigInteger i = new BigInteger(1, m.digest());
        String s = String.format("%1$040X", i);
        return s.substring(32);
    }

    private boolean hasDropboxApp(Intent intent) {
        PackageManager manager = getPackageManager();

        List<ResolveInfo> infos = manager.queryIntentActivities(intent, 0);
        if (null == infos || 1 != infos.size()) {
            // The official app doesn't exist, or only an older version
            // is available, or multiple activities are confusing us.
            return false;
        } else {
            // The official app exists. Make sure it's the correct one by
            // checking signing keys.
            ResolveInfo resolveInfo = manager.resolveActivity(intent, 0);
            if (resolveInfo == null) {
                return false;
            }

            final PackageInfo packageInfo;
            try {
                packageInfo = manager.getPackageInfo(
                        resolveInfo.activityInfo.packageName,
                        PackageManager.GET_SIGNATURES);
            } catch (NameNotFoundException e) {
                return false;
            }

            for (Signature signature : packageInfo.signatures) {
                for (String dbSignature : DROPBOX_APP_SIGNATURES) {
                    if (dbSignature.equals(signature.toCharsString())) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    private void startWebAuth(String state) {
        String path = "/connect";
        Locale locale = Locale.getDefault();

        String[] params = {
            "locale", locale.getLanguage()+"_"+locale.getCountry(),
            "k", mAppKey,
            "s", getConsumerSig(),
            "api", mApiType,
            "state", state};

        String url = RESTUtility.buildURL(mWebHost, DropboxAPI.VERSION, path, params);

        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
        startActivity(intent);
    }

    private static final String[] DROPBOX_APP_SIGNATURES = {
        "308202223082018b02044bd207bd300d06092a864886f70d01010405003058310b3" +
        "009060355040613025553310b300906035504081302434131163014060355040713" +
        "0d53616e204672616e636973636f3110300e060355040a130744726f70626f78311" +
        "2301006035504031309546f6d204d65796572301e170d3130303432333230343930" +
        "315a170d3430303431353230343930315a3058310b3009060355040613025553310" +
        "b3009060355040813024341311630140603550407130d53616e204672616e636973" +
        "636f3110300e060355040a130744726f70626f783112301006035504031309546f6" +
        "d204d6579657230819f300d06092a864886f70d010101050003818d003081890281" +
        "8100ac1595d0ab278a9577f0ca5a14144f96eccde75f5616f36172c562fab0e98c4" +
        "8ad7d64f1091c6cc11ce084a4313d522f899378d312e112a748827545146a779def" +
        "a7c31d8c00c2ed73135802f6952f59798579859e0214d4e9c0554b53b26032a4d2d" +
        "fc2f62540d776df2ea70e2a6152945fb53fef5bac5344251595b729d48102030100" +
        "01300d06092a864886f70d01010405000381810055c425d94d036153203dc0bbeb3" +
        "516f94563b102fff39c3d4ed91278db24fc4424a244c2e59f03bbfea59404512b8b" +
        "f74662f2a32e37eafa2ac904c31f99cfc21c9ff375c977c432d3b6ec22776f28767" +
        "d0f292144884538c3d5669b568e4254e4ed75d9054f75229ac9d4ccd0b7c3c74a34" +
        "f07b7657083b2aa76225c0c56ffc",
        "308201e53082014ea00302010202044e17e115300d06092a864886f70d010105050" +
        "03037310b30090603550406130255533110300e060355040a1307416e64726f6964" +
        "311630140603550403130d416e64726f6964204465627567301e170d31313037303" +
        "93035303331375a170d3431303730313035303331375a3037310b30090603550406" +
        "130255533110300e060355040a1307416e64726f6964311630140603550403130d4" +
        "16e64726f696420446562756730819f300d06092a864886f70d010101050003818d" +
        "003081890281810096759fe5abea6a0757039b92adc68d672efa84732c3f959408e" +
        "12efa264545c61f23141026a6d01eceeeaa13ec7087087e5894a3363da8bf5c69ed" +
        "93657a6890738a80998e4ca22dc94848f30e2d0e1890000ae2cddf543b20c0c3828" +
        "deca6c7944b5ecd21a9d18c988b2b3e54517dafbc34b48e801bb1321e0fa49e4d57" +
        "5d7f0203010001300d06092a864886f70d0101050500038181002b6d4b65bcfa6ec" +
        "7bac97ae6d878064d47b3f9f8da654995b8ef4c385bc4fbfbb7a987f60783ef0348" +
        "760c0708acd4b7e63f0235c35a4fbcd5ec41b3b4cb295feaa7d5c27fa562a02562b" +
        "7e1f4776b85147be3e295714986c4a9a07183f48ea09ae4d3ea31b88d0016c65b93" +
        "526b9c45f2967c3d28dee1aff5a5b29b9c2c8639"};

    private String createStateNonce() {
        final int NONCE_BYTES = 16; // 128 bits of randomness.
        byte randomBytes[] = new byte[NONCE_BYTES];
        getSecureRandom().nextBytes(randomBytes);
        StringBuilder sb = new StringBuilder();
        if (mAppSecret == null) {
            sb.append("oauth2:");
        }
        for (int i = 0; i < NONCE_BYTES; ++i) {
            sb.append(String.format("%02x", (randomBytes[i]&0xff)));
        }
        return sb.toString();
    }
}




Java Source Code List

.CopyBetweenAccounts.java
.SearchCache.java
com.dropbox.android.sample.DBRoulette.java
com.dropbox.android.sample.DownloadRandomPicture.java
com.dropbox.android.sample.UploadPicture.java
com.dropbox.client2.DropboxAPI.java
com.dropbox.client2.ProgressListener.java
com.dropbox.client2.RESTUtility.java
com.dropbox.client2.SdkVersion.java
com.dropbox.client2.SecureSSLSocketFactory.java
com.dropbox.client2.android.AndroidAuthSession.java
com.dropbox.client2.android.AuthActivity.java
com.dropbox.client2.exception.DropboxException.java
com.dropbox.client2.exception.DropboxFileSizeException.java
com.dropbox.client2.exception.DropboxIOException.java
com.dropbox.client2.exception.DropboxLocalStorageFullException.java
com.dropbox.client2.exception.DropboxParseException.java
com.dropbox.client2.exception.DropboxPartialFileException.java
com.dropbox.client2.exception.DropboxProxyChangeException.java
com.dropbox.client2.exception.DropboxSSLException.java
com.dropbox.client2.exception.DropboxServerException.java
com.dropbox.client2.exception.DropboxUnlinkedException.java
com.dropbox.client2.jsonextract.JsonBase.java
com.dropbox.client2.jsonextract.JsonExtractionException.java
com.dropbox.client2.jsonextract.JsonExtractor.java
com.dropbox.client2.jsonextract.JsonList.java
com.dropbox.client2.jsonextract.JsonMap.java
com.dropbox.client2.jsonextract.JsonThing.java
com.dropbox.client2.session.AbstractSession.java
com.dropbox.client2.session.AccessTokenPair.java
com.dropbox.client2.session.AppKeyPair.java
com.dropbox.client2.session.RequestTokenPair.java
com.dropbox.client2.session.Session.java
com.dropbox.client2.session.TokenPair.java
com.dropbox.client2.session.WebAuthSession.java
com.dropbox.client2.session.WebOAuth2Session.java