Java tutorial
// Copyright Microsoft Open Technologies, Inc. // // All Rights Reserved // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS // OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION // ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A // PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. // // See the Apache License, Version 2.0 for the specific language // governing permissions and limitations under the License. package com.microsoft.aad.adal; import java.io.IOException; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.DigestException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.UnrecoverableEntryException; import java.security.cert.CertificateException; import java.security.spec.InvalidKeySpecException; import java.util.UUID; import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import android.accounts.Account; import android.accounts.AccountAuthenticatorResponse; import android.accounts.AccountManager; import android.annotation.SuppressLint; import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.Window; import android.webkit.CookieManager; import android.webkit.CookieSyncManager; import android.webkit.WebView; import com.google.gson.Gson; /** * Authentication Activity to launch {@link WebView} for authentication. */ @SuppressLint({ "SetJavaScriptEnabled", "ClickableViewAccessibility" }) public class AuthenticationActivity extends Activity { static final int BACK_PRESSED_CANCEL_DIALOG_STEPS = -2; private static final String TAG = "AuthenticationActivity"; private boolean mRegisterReceiver = false; private WebView mWebView; private String mStartUrl; private ProgressDialog mSpinner; private String mRedirectUrl; private AuthenticationRequest mAuthRequest; // Broadcast receiver for cancel private ActivityBroadcastReceiver mReceiver = null; private String mCallingPackage; private int mWaitingRequestId; private int mCallingUID; private AccountAuthenticatorResponse mAccountAuthenticatorResponse = null; private Bundle mAuthenticatorResultBundle = null; private IWebRequestHandler mWebRequestHandler = new WebRequestHandler(); private IJWSBuilder mJWSBuilder = new JWSBuilder(); private String mQueryParameters; private boolean mPkeyAuthRedirect = false; // Broadcast receiver is needed to cancel outstanding AuthenticationActivity // for this AuthenticationContext since each instance of context can have // one active activity private class ActivityBroadcastReceiver extends android.content.BroadcastReceiver { private int mWaitingRequestId = -1; @Override public void onReceive(Context context, Intent intent) { Logger.v(TAG, "ActivityBroadcastReceiver onReceive"); if (intent.getAction().equalsIgnoreCase(AuthenticationConstants.Browser.ACTION_CANCEL)) { try { Logger.v(TAG, "ActivityBroadcastReceiver onReceive action is for cancelling Authentication Activity"); int cancelRequestId = intent.getIntExtra(AuthenticationConstants.Browser.REQUEST_ID, 0); if (cancelRequestId == mWaitingRequestId) { Logger.v(TAG, "Waiting requestId is same and cancelling this activity"); AuthenticationActivity.this.finish(); // no need to send result back to activity. It is // cancelled // and callback will be called after this request. } } catch (Exception ex) { Logger.e(TAG, "ActivityBroadcastReceiver onReceive exception", ExceptionExtensions.getExceptionMessage(ex), ADALError.BROADCAST_RECEIVER_ERROR); } } } } @SuppressLint("SetJavaScriptEnabled") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView( this.getResources().getIdentifier("activity_authentication", "layout", this.getPackageName())); CookieSyncManager.createInstance(getApplicationContext()); CookieSyncManager.getInstance().sync(); CookieManager cookieManager = CookieManager.getInstance(); cookieManager.setAcceptCookie(true); // Get the message from the intent mAuthRequest = getAuthenticationRequestFromIntent(getIntent()); if (mAuthRequest == null) { Log.d(TAG, "Request item is null, so it returns to caller"); Intent resultIntent = new Intent(); resultIntent.putExtra(AuthenticationConstants.Browser.RESPONSE_ERROR_CODE, AuthenticationConstants.Browser.WEBVIEW_INVALID_REQUEST); resultIntent.putExtra(AuthenticationConstants.Browser.RESPONSE_ERROR_MESSAGE, "Intent does not have request details"); returnToCaller(AuthenticationConstants.UIResponse.BROWSER_CODE_ERROR, resultIntent); return; } if (mAuthRequest.getAuthority() == null || mAuthRequest.getAuthority().isEmpty()) { returnError(ADALError.ARGUMENT_EXCEPTION, AuthenticationConstants.Broker.ACCOUNT_AUTHORITY); return; } if (mAuthRequest.getResource() == null || mAuthRequest.getResource().isEmpty()) { returnError(ADALError.ARGUMENT_EXCEPTION, AuthenticationConstants.Broker.ACCOUNT_RESOURCE); return; } if (mAuthRequest.getClientId() == null || mAuthRequest.getClientId().isEmpty()) { returnError(ADALError.ARGUMENT_EXCEPTION, AuthenticationConstants.Broker.ACCOUNT_CLIENTID_KEY); return; } if (mAuthRequest.getRedirectUri() == null || mAuthRequest.getRedirectUri().isEmpty()) { returnError(ADALError.ARGUMENT_EXCEPTION, AuthenticationConstants.Broker.ACCOUNT_REDIRECT); return; } mRedirectUrl = mAuthRequest.getRedirectUri(); Logger.v(TAG, "OnCreate redirectUrl:" + mRedirectUrl); // Create the Web View to show the page mWebView = (WebView) findViewById( this.getResources().getIdentifier("webView1", "id", this.getPackageName())); Logger.v(TAG, "User agent:" + mWebView.getSettings().getUserAgentString()); mStartUrl = "about:blank"; try { Oauth2 oauth = new Oauth2(mAuthRequest); mStartUrl = oauth.getCodeRequestUrl(); mQueryParameters = oauth.getAuthorizationEndpointQueryParameters(); } catch (UnsupportedEncodingException e) { Log.d(TAG, e.getMessage()); Intent resultIntent = new Intent(); resultIntent.putExtra(AuthenticationConstants.Browser.RESPONSE_REQUEST_INFO, mAuthRequest); returnToCaller(AuthenticationConstants.UIResponse.BROWSER_CODE_ERROR, resultIntent); return; } // Create the broadcast receiver for cancel Logger.v(TAG, "Init broadcastReceiver with requestId:" + mAuthRequest.getRequestId() + " " + mAuthRequest.getLogInfo()); mReceiver = new ActivityBroadcastReceiver(); mReceiver.mWaitingRequestId = mAuthRequest.getRequestId(); LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, new IntentFilter(AuthenticationConstants.Browser.ACTION_CANCEL)); String userAgent = mWebView.getSettings().getUserAgentString(); mWebView.getSettings() .setUserAgentString(userAgent + AuthenticationConstants.Broker.CLIENT_TLS_NOT_SUPPORTED); userAgent = mWebView.getSettings().getUserAgentString(); Logger.v(TAG, "UserAgent:" + userAgent); if (isBrokerRequest(getIntent())) { // This activity is started from calling app and running in // Authenticator's process mCallingPackage = getCallingPackage(); Logger.i(TAG, "It is a broker request for package:" + mCallingPackage, ""); if (mCallingPackage == null) { Logger.v(TAG, "startActivityForResult is not used to call this activity"); Intent resultIntent = new Intent(); resultIntent.putExtra(AuthenticationConstants.Browser.RESPONSE_ERROR_CODE, AuthenticationConstants.Browser.WEBVIEW_INVALID_REQUEST); resultIntent.putExtra(AuthenticationConstants.Browser.RESPONSE_ERROR_MESSAGE, "startActivityForResult is not used to call this activity"); returnToCaller(AuthenticationConstants.UIResponse.BROWSER_CODE_ERROR, resultIntent); return; } mAccountAuthenticatorResponse = getIntent() .getParcelableExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE); if (mAccountAuthenticatorResponse != null) { mAccountAuthenticatorResponse.onRequestContinued(); } PackageHelper info = new PackageHelper(AuthenticationActivity.this); mCallingPackage = getCallingPackage(); mCallingUID = info.getUIDForPackage(mCallingPackage); String signatureDigest = info.getCurrentSignatureForPackage(mCallingPackage); mStartUrl = getBrokerStartUrl(mStartUrl, mCallingPackage, signatureDigest); if (!isCallerBrokerInstaller()) { Logger.v(TAG, "Caller needs to be verified using special redirectUri"); mRedirectUrl = PackageHelper.getBrokerRedirectUrl(mCallingPackage, signatureDigest); } Logger.v(TAG, "OnCreate redirectUrl:" + mRedirectUrl + " startUrl:" + mStartUrl + " calling package:" + mCallingPackage + " signatureDigest:" + signatureDigest + " current Context Package: " + getPackageName()); } mRegisterReceiver = false; final String postUrl = mStartUrl; Logger.i(TAG, "OnCreate startUrl:" + mStartUrl + " calling package:" + mCallingPackage, " device:" + android.os.Build.VERSION.RELEASE + " " + android.os.Build.MANUFACTURER + android.os.Build.MODEL); setupWebView(mRedirectUrl, mQueryParameters, mAuthRequest); if (savedInstanceState == null) { mWebView.post(new Runnable() { @Override public void run() { // load blank first to avoid error for not loading webview mWebView.loadUrl("about:blank"); mWebView.loadUrl(postUrl); } }); } else { Logger.v(TAG, "Reuse webview"); } } private boolean isCallerBrokerInstaller() { // Allow intune's signature check PackageHelper info = new PackageHelper(AuthenticationActivity.this); String packageName = getCallingPackage(); if (!StringExtensions.IsNullOrBlank(packageName)) { if (packageName.equals(AuthenticationSettings.INSTANCE.getBrokerPackageName())) { Logger.v(TAG, "isCallerBrokerInstaller: same package as broker " + packageName); return true; } String signature = info.getCurrentSignatureForPackage(packageName); Logger.v(TAG, "isCallerBrokerInstaller: Check signature for " + packageName + " signature:" + signature + " brokerSignature:" + AuthenticationSettings.INSTANCE.getBrokerSignature()); return signature.equals(AuthenticationSettings.INSTANCE.getBrokerSignature()) || signature.equals(AuthenticationConstants.Broker.AZURE_AUTHENTICATOR_APP_SIGNATURE); } return false; } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // Save the state of the WebView mWebView.saveState(outState); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); // Restore the state of the WebView mWebView.restoreState(savedInstanceState); } private void setupWebView(String redirect, String queryParam, AuthenticationRequest request) { mWebView.getSettings().setJavaScriptEnabled(true); mWebView.requestFocus(View.FOCUS_DOWN); // Set focus to the view for touch event mWebView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP) { if (!view.hasFocus()) { view.requestFocus(); } } return false; } }); mWebView.getSettings().setLoadWithOverviewMode(true); mWebView.getSettings().setDomStorageEnabled(true); mWebView.getSettings().setUseWideViewPort(true); mWebView.getSettings().setBuiltInZoomControls(true); mWebView.setWebViewClient(new CustomWebViewClient()); mWebView.setVisibility(View.INVISIBLE); } private AuthenticationRequest getAuthenticationRequestFromIntent(Intent callingIntent) { AuthenticationRequest authRequest = null; if (isBrokerRequest(callingIntent)) { Logger.v(TAG, "It is a broker request. Get request info from bundle extras."); String authority = callingIntent.getStringExtra(AuthenticationConstants.Broker.ACCOUNT_AUTHORITY); String resource = callingIntent.getStringExtra(AuthenticationConstants.Broker.ACCOUNT_RESOURCE); String redirect = callingIntent.getStringExtra(AuthenticationConstants.Broker.ACCOUNT_REDIRECT); String loginhint = callingIntent.getStringExtra(AuthenticationConstants.Broker.ACCOUNT_LOGIN_HINT); String accountName = callingIntent.getStringExtra(AuthenticationConstants.Broker.ACCOUNT_NAME); String clientidKey = callingIntent.getStringExtra(AuthenticationConstants.Broker.ACCOUNT_CLIENTID_KEY); String correlationId = callingIntent .getStringExtra(AuthenticationConstants.Broker.ACCOUNT_CORRELATIONID); String prompt = callingIntent.getStringExtra(AuthenticationConstants.Broker.ACCOUNT_PROMPT); PromptBehavior promptBehavior = PromptBehavior.Auto; if (!StringExtensions.IsNullOrBlank(prompt)) { promptBehavior = PromptBehavior.valueOf(prompt); } mWaitingRequestId = callingIntent.getIntExtra(AuthenticationConstants.Browser.REQUEST_ID, 0); UUID correlationIdParsed = null; if (!StringExtensions.IsNullOrBlank(correlationId)) { try { correlationIdParsed = UUID.fromString(correlationId); } catch (IllegalArgumentException ex) { correlationIdParsed = null; Logger.e(TAG, "CorrelationId is malformed: " + correlationId, "", ADALError.CORRELATION_ID_FORMAT); } } authRequest = new AuthenticationRequest(authority, resource, clientidKey, redirect, loginhint, correlationIdParsed); authRequest.setBrokerAccountName(accountName); authRequest.setPrompt(promptBehavior); authRequest.setRequestId(mWaitingRequestId); } else { Serializable request = callingIntent .getSerializableExtra(AuthenticationConstants.Browser.REQUEST_MESSAGE); if (request instanceof AuthenticationRequest) { authRequest = (AuthenticationRequest) request; } } return authRequest; } /** * Return error to caller and finish this activity. */ private void returnError(ADALError errorCode, String argument) { // Set result back to account manager call Log.w(TAG, "Argument error:" + argument); Intent resultIntent = new Intent(); // TODO only send adalerror from activity side as int resultIntent.putExtra(AuthenticationConstants.Browser.RESPONSE_ERROR_CODE, errorCode.name()); resultIntent.putExtra(AuthenticationConstants.Browser.RESPONSE_ERROR_MESSAGE, argument); if (mAuthRequest != null) { resultIntent.putExtra(AuthenticationConstants.Browser.REQUEST_ID, mWaitingRequestId); resultIntent.putExtra(AuthenticationConstants.Browser.RESPONSE_REQUEST_INFO, mAuthRequest); } this.setResult(AuthenticationConstants.UIResponse.BROWSER_CODE_ERROR, resultIntent); this.finish(); } private void returnAuthenticationException(final AuthenticationException e) { Intent resultIntent = new Intent(); resultIntent.putExtra(AuthenticationConstants.Browser.RESPONSE_AUTHENTICATION_EXCEPTION, e); if (mAuthRequest != null) { resultIntent.putExtra(AuthenticationConstants.Browser.REQUEST_ID, mWaitingRequestId); resultIntent.putExtra(AuthenticationConstants.Browser.RESPONSE_REQUEST_INFO, mAuthRequest); } this.setResult(AuthenticationConstants.UIResponse.BROWSER_CODE_AUTHENTICATION_EXCEPTION, resultIntent); this.finish(); } private String getBrokerStartUrl(String loadUrl, String packageName, String signatureDigest) { if (!StringExtensions.IsNullOrBlank(packageName) && !StringExtensions.IsNullOrBlank(signatureDigest)) { try { return loadUrl + "&package_name=" + URLEncoder.encode(packageName, AuthenticationConstants.ENCODING_UTF8) + "&signature=" + URLEncoder.encode(signatureDigest, AuthenticationConstants.ENCODING_UTF8); } catch (UnsupportedEncodingException e) { // This encoding issue will happen at the beginning of API call, // if it is not supported on this device. ADAL uses one encoding // type. Log.e(TAG, "Encoding", e); } } return loadUrl; } private boolean isBrokerRequest(Intent callingIntent) { Logger.v(TAG, "Packagename:" + getPackageName() + " Broker packagename:" + AuthenticationSettings.INSTANCE.getBrokerPackageName() + " Calling packagename:" + getCallingPackage()); // Intent should have a flag and activity is hosted inside broker return callingIntent != null && !StringExtensions .IsNullOrBlank(callingIntent.getStringExtra(AuthenticationConstants.Broker.BROKER_REQUEST)); } /** * Activity sets result to go back to the caller. * * @param resultCode * @param data */ private void returnToCaller(int resultCode, Intent data) { Logger.v(TAG, "Return To Caller:" + resultCode); displaySpinner(false); if (data == null) { data = new Intent(); } if (mAuthRequest != null) { // set request id related to this response to send the delegateId Logger.v(TAG, "Return To Caller REQUEST_ID:" + mAuthRequest.getRequestId()); data.putExtra(AuthenticationConstants.Browser.REQUEST_ID, mAuthRequest.getRequestId()); } else { Logger.w(TAG, "Request object is null", "", ADALError.ACTIVITY_REQUEST_INTENT_DATA_IS_NULL); } setResult(resultCode, data); this.finish(); } @Override protected void onPause() { Logger.v(TAG, "AuthenticationActivity onPause unregister receiver"); super.onPause(); // Unregister the cancel action listener from the local broadcast // manager since activity is not visible if (mReceiver != null) { LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); } mRegisterReceiver = true; if (mSpinner != null) { Logger.v(TAG, "Spinner at onPause will dismiss"); mSpinner.dismiss(); } } @Override protected void onResume() { super.onResume(); Logger.v(TAG, "onResume"); // It can come here from onCreate, onRestart or onPause. // Don't load url again since it will send another 2FA request if (mRegisterReceiver) { Logger.v(TAG, "Webview onResume will register receiver:" + mStartUrl); if (mReceiver != null) { Logger.v(TAG, "Webview onResume register broadcast receiver for requestId" + mReceiver.mWaitingRequestId); LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, new IntentFilter(AuthenticationConstants.Browser.ACTION_CANCEL)); } } mRegisterReceiver = false; // Spinner dialog to show some message while it is loading mSpinner = new ProgressDialog(this); mSpinner.requestWindowFeature(Window.FEATURE_NO_TITLE); mSpinner.setMessage( this.getText(this.getResources().getIdentifier("app_loading", "string", this.getPackageName()))); } @Override protected void onRestart() { Logger.v(TAG, "AuthenticationActivity onRestart"); super.onRestart(); mRegisterReceiver = true; } @Override public void onBackPressed() { Logger.v(TAG, "Back button is pressed"); // User should be able to click back button to cancel in case pkeyauth // happen. if (mPkeyAuthRedirect || !mWebView.canGoBackOrForward(BACK_PRESSED_CANCEL_DIALOG_STEPS)) { // counting blank page as well cancelRequest(); } else { // Don't use default back pressed action, since user can go back in // webview mWebView.goBack(); } } private void cancelRequest() { Logger.v(TAG, "Sending intent to cancel authentication activity"); Intent resultIntent = new Intent(); returnToCaller(AuthenticationConstants.UIResponse.BROWSER_CODE_CANCEL, resultIntent); } class CustomWebViewClient extends BasicWebViewClient { public CustomWebViewClient() { super(AuthenticationActivity.this, mRedirectUrl, mQueryParameters, mAuthRequest); } public void processRedirectUrl(final WebView view, String url) { if (!isBrokerRequest(getIntent())) { // It is pointing to redirect. Final url can be processed to // get the code or error. Logger.i(TAG, "It is not a broker request", ""); Intent resultIntent = new Intent(); resultIntent.putExtra(AuthenticationConstants.Browser.RESPONSE_FINAL_URL, url); resultIntent.putExtra(AuthenticationConstants.Browser.RESPONSE_REQUEST_INFO, mAuthRequest); returnToCaller(AuthenticationConstants.UIResponse.BROWSER_CODE_COMPLETE, resultIntent); view.stopLoading(); } else { Logger.i(TAG, "It is a broker request", ""); displaySpinnerWithMessage(AuthenticationActivity.this.getText(AuthenticationActivity.this .getResources().getIdentifier("broker_processing", "string", getPackageName()))); view.stopLoading(); // do async task and show spinner while exchanging code for // access token new TokenTask(mWebRequestHandler, mAuthRequest, mCallingPackage, mCallingUID).execute(url); } } public boolean processInvalidUrl(final WebView view, String url) { if (isBrokerRequest(getIntent()) && url.startsWith(AuthenticationConstants.Broker.REDIRECT_PREFIX)) { returnError(ADALError.DEVELOPER_REDIRECTURI_INVALID, String.format( "The RedirectUri is not as expected. Received %s and expected %s", url, mRedirectUrl)); view.stopLoading(); return true; } return false; } public void showSpinner(boolean status) { displaySpinner(status); } @Override public void sendResponse(int returnCode, Intent responseIntent) { returnToCaller(returnCode, responseIntent); } @Override public void cancelWebViewRequest() { cancelRequest(); } @Override public void setPKeyAuthStatus(boolean status) { mPkeyAuthRedirect = status; } @Override public void postRunnable(Runnable item) { mWebView.post(item); } } /** * handle spinner display. * * @param show */ private void displaySpinner(boolean show) { if (!AuthenticationActivity.this.isFinishing() && !AuthenticationActivity.this.isChangingConfigurations() && mSpinner != null) { Logger.v(TAG, "displaySpinner:" + show + " showing:" + mSpinner.isShowing()); if (show && !mSpinner.isShowing()) { mSpinner.show(); } if (!show && mSpinner.isShowing()) { mSpinner.dismiss(); } } } private void displaySpinnerWithMessage(CharSequence charSequence) { if (!AuthenticationActivity.this.isFinishing() && mSpinner != null) { mSpinner.show(); mSpinner.setMessage(charSequence); } } private void returnResult(int resultcode, Intent intent) { // Set result back to account manager call this.setAccountAuthenticatorResult(intent.getExtras()); this.setResult(resultcode, intent); this.finish(); } /** * Sends the result or a Constants.ERROR_CODE_CANCELED error if a result * isn't present. */ @Override public void finish() { // Added here to make Authenticator work with one common code base if (isBrokerRequest(getIntent()) && mAccountAuthenticatorResponse != null) { // send the result bundle back if set, otherwise send an error. Logger.v(TAG, "It is a broker request"); if (mAuthenticatorResultBundle != null) { mAccountAuthenticatorResponse.onResult(mAuthenticatorResultBundle); } else { mAccountAuthenticatorResponse.onError(AccountManager.ERROR_CODE_CANCELED, "canceled"); } mAccountAuthenticatorResponse = null; } super.finish(); } /** * Set the result that is to be sent as the result of the request that * caused this Activity to be launched. If result is null or this method is * never called then the request will be canceled. * * @param result this is returned as the result of the * AbstractAccountAuthenticator request */ private final void setAccountAuthenticatorResult(Bundle result) { mAuthenticatorResultBundle = result; } /** * Processes the authorization code to get token and stores inside the * Account before returning back to the calling app. App does not receive * refresh tokens. Calling app does not have access to * setUserData/getUserData inside the AccountManager. This is used only for * broker related call. */ class TokenTask extends AsyncTask<String, String, TokenTaskResult> { String mPackageName; int mAppCallingUID; AuthenticationRequest mRequest; AccountManager mAccountManager; IWebRequestHandler mRequestHandler; public TokenTask() { } public TokenTask(IWebRequestHandler webHandler, final AuthenticationRequest request, final String packagename, final int callingUID) { mRequestHandler = webHandler; mRequest = request; mPackageName = packagename; mAppCallingUID = callingUID; mAccountManager = AccountManager.get(AuthenticationActivity.this); } @Override protected TokenTaskResult doInBackground(String... urlItems) { Oauth2 oauthRequest = new Oauth2(mRequest, mRequestHandler, mJWSBuilder); TokenTaskResult result = new TokenTaskResult(); try { result.taskResult = oauthRequest.getToken(urlItems[0]); Logger.v(TAG, "TokenTask processed the result. " + mRequest.getLogInfo()); } catch (Exception exc) { Logger.e(TAG, "Error in processing code to get a token. " + mRequest.getLogInfo(), "Request url:" + urlItems[0], ADALError.AUTHORIZATION_CODE_NOT_EXCHANGED_FOR_TOKEN, exc); result.taskException = exc; } if (result != null && result.taskResult != null && result.taskResult.getAccessToken() != null) { Logger.v(TAG, "Setting account:" + mRequest.getLogInfo()); // Record account in the AccountManager service try { setAccount(result); } catch (Exception exc) { Logger.e(TAG, "Error in setting the account" + mRequest.getLogInfo(), "", ADALError.BROKER_ACCOUNT_SAVE_FAILED, exc); result.taskException = exc; } } return result; } private String getBrokerAppCacheKey(StorageHelper cryptoHelper, String cacheKey) throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException, IOException { // include UID in the key for broker to store caches for different // apps under same account entry String digestKey = StringExtensions .createHash(AuthenticationConstants.Broker.USERDATA_UID_KEY + mAppCallingUID + cacheKey); Logger.v(TAG, "Cache key original:" + cacheKey + " digestKey:" + digestKey + " calling app UID:" + mAppCallingUID); return digestKey; } private void appendAppUIDToAccount(StorageHelper cryptoHelper, Account account) throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException, IOException, KeyStoreException, CertificateException, NoSuchProviderException, UnrecoverableEntryException, DigestException { String appIdList = mAccountManager.getUserData(account, AuthenticationConstants.Broker.ACCOUNT_UID_CACHES); if (appIdList == null) { appIdList = ""; } else { try { appIdList = cryptoHelper.decrypt(appIdList); } catch (Exception ex) { Logger.e(TAG, "appUIDList failed to decrypt", "appIdList:" + appIdList, ADALError.ENCRYPTION_FAILED, ex); appIdList = ""; Logger.i(TAG, "Reset the appUIDlist", ""); } } Logger.i(TAG, "Add calling UID:" + mAppCallingUID, "appIdList:" + appIdList); if (!appIdList.contains(AuthenticationConstants.Broker.USERDATA_UID_KEY + mAppCallingUID)) { Logger.i(TAG, "Account has new calling UID:" + mAppCallingUID, ""); mAccountManager.setUserData(account, AuthenticationConstants.Broker.ACCOUNT_UID_CACHES, cryptoHelper .encrypt(appIdList + AuthenticationConstants.Broker.USERDATA_UID_KEY + mAppCallingUID)); } } private void setAccount(final TokenTaskResult result) throws InvalidKeyException, InvalidKeySpecException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException { // TODO Add token logging // TODO update for new cache logic // Authenticator sets the account here and stores the tokens. try { String name = mRequest.getBrokerAccountName(); Account[] accountList = mAccountManager .getAccountsByType(AuthenticationConstants.Broker.BROKER_ACCOUNT_TYPE); if (accountList == null || accountList.length != 1) { result.taskResult = null; result.taskException = new AuthenticationException(ADALError.BROKER_SINGLE_USER_EXPECTED); return; } Account newaccount = accountList[0]; // Single user in authenticator is already created. // This is only registering UID for the app UserInfo userinfo = result.taskResult.getUserInfo(); if (userinfo == null || StringExtensions.IsNullOrBlank(userinfo.getUserId())) { // return userid in the userinfo and use only account name // for all fields Logger.i(TAG, "Set userinfo from account", ""); result.taskResult.setUserInfo(new UserInfo(name, name, "", "", name)); mRequest.setLoginHint(name); } else { Logger.i(TAG, "Saving userinfo to account", ""); mAccountManager.setUserData(newaccount, AuthenticationConstants.Broker.ACCOUNT_USERINFO_USERID, userinfo.getUserId()); mAccountManager.setUserData(newaccount, AuthenticationConstants.Broker.ACCOUNT_USERINFO_GIVEN_NAME, userinfo.getGivenName()); mAccountManager.setUserData(newaccount, AuthenticationConstants.Broker.ACCOUNT_USERINFO_FAMILY_NAME, userinfo.getFamilyName()); mAccountManager.setUserData(newaccount, AuthenticationConstants.Broker.ACCOUNT_USERINFO_IDENTITY_PROVIDER, userinfo.getIdentityProvider()); mAccountManager.setUserData(newaccount, AuthenticationConstants.Broker.ACCOUNT_USERINFO_USERID_DISPLAYABLE, userinfo.getDisplayableId()); } result.accountName = name; Logger.i(TAG, "Setting account. Account name: " + name + " package:" + mCallingPackage + " calling app UID:" + mAppCallingUID, ""); // Cache logic will be changed based on latest logic // This is currently keeping accesstoken and MRRT separate // Encrypted Results are saved to AccountManager Service // sqllite database. Only Authenticator and similar UID can // access. Gson gson = new Gson(); Logger.i(TAG, "app context:" + getApplicationContext().getPackageName() + " context:" + AuthenticationActivity.this.getPackageName() + " calling packagename:" + getCallingPackage(), ""); StorageHelper cryptoHelper = new StorageHelper(getApplicationContext()); if (AuthenticationSettings.INSTANCE.getSecretKeyData() == null) { Logger.i(TAG, "setAccount: user key is null", ""); } TokenCacheItem item = new TokenCacheItem(mRequest, result.taskResult, false); String json = gson.toJson(item); String encrypted = cryptoHelper.encrypt(json); // Single user and cache is stored per account String key = CacheKey.createCacheKey(mRequest, null); saveCacheKey(key, newaccount, mAppCallingUID); mAccountManager.setUserData(newaccount, getBrokerAppCacheKey(cryptoHelper, key), encrypted); if (result.taskResult.getIsMultiResourceRefreshToken()) { // ADAL stores MRRT refresh token separately TokenCacheItem itemMRRT = new TokenCacheItem(mRequest, result.taskResult, true); json = gson.toJson(itemMRRT); encrypted = cryptoHelper.encrypt(json); key = CacheKey.createMultiResourceRefreshTokenKey(mRequest, null); saveCacheKey(key, newaccount, mAppCallingUID); mAccountManager.setUserData(newaccount, getBrokerAppCacheKey(cryptoHelper, key), encrypted); } // Record calling UID for this account so that app can get token // in the background call without requiring server side // validation Logger.i(TAG, "Set calling uid:" + mAppCallingUID, ""); appendAppUIDToAccount(cryptoHelper, newaccount); } catch (NoSuchAlgorithmException e) { Logger.e(TAG, "Algorithm does not exist in the device", "", ADALError.DEVICE_CACHE_IS_NOT_WORKING, e); result.taskException = e; } catch (NoSuchPaddingException e) { Logger.e(TAG, "Padding type does not exist in the device", "", ADALError.DEVICE_CACHE_IS_NOT_WORKING, e); result.taskException = e; } catch (KeyStoreException e) { Logger.e(TAG, "Key store type is not supported", "", ADALError.DEVICE_CACHE_IS_NOT_WORKING, e); result.taskException = e; } catch (CertificateException e) { Logger.e(TAG, "Certificate exception", "", ADALError.DEVICE_CACHE_IS_NOT_WORKING, e); result.taskException = e; } catch (NoSuchProviderException e) { Logger.e(TAG, "Requested security provider does not exists in the device", "", ADALError.DEVICE_CACHE_IS_NOT_WORKING, e); result.taskException = e; } catch (UnrecoverableEntryException e) { Logger.e(TAG, "Key entry is not recoverable", "", ADALError.DEVICE_CACHE_IS_NOT_WORKING, e); result.taskException = e; } catch (DigestException e) { Logger.e(TAG, "Digest is not valid", "", ADALError.DEVICE_CACHE_IS_NOT_WORKING, e); result.taskException = e; } } private void saveCacheKey(String key, Account cacheAccount, int callingUID) { Logger.v(TAG, "Get CacheKeys for account"); // Store cachekeys for each UID // Activity has access to packagename and UID, but background call // in getAuthToken only knows about UID String keylist = mAccountManager.getUserData(cacheAccount, AuthenticationConstants.Broker.USERDATA_CALLER_CACHEKEYS + callingUID); if (keylist == null) { keylist = ""; } if (!keylist.contains(AuthenticationConstants.Broker.CALLER_CACHEKEY_PREFIX + key)) { Logger.v(TAG, "Account does not have this cache key:" + key + " It will save it to accoun for the callerUID:" + callingUID); keylist += AuthenticationConstants.Broker.CALLER_CACHEKEY_PREFIX + key; mAccountManager.setUserData(cacheAccount, AuthenticationConstants.Broker.USERDATA_CALLER_CACHEKEYS + callingUID, keylist); Logger.v(TAG, "keylist:" + keylist); } } @Override protected void onPostExecute(TokenTaskResult result) { Logger.v(TAG, "Token task returns the result"); displaySpinner(false); Intent intent = new Intent(); if (result.taskResult != null) { intent.putExtra(AuthenticationConstants.Browser.REQUEST_ID, mWaitingRequestId); intent.putExtra(AuthenticationConstants.Broker.ACCOUNT_ACCESS_TOKEN, result.taskResult.getAccessToken()); intent.putExtra(AuthenticationConstants.Broker.ACCOUNT_NAME, result.accountName); intent.putExtra(AuthenticationConstants.Broker.ACCOUNT_EXPIREDATE, result.taskResult.getExpiresOn().getTime()); if (result.taskResult.getUserInfo() != null) { intent.putExtra(AuthenticationConstants.Broker.ACCOUNT_USERINFO_USERID, result.taskResult.getUserInfo().getUserId()); intent.putExtra(AuthenticationConstants.Broker.ACCOUNT_USERINFO_GIVEN_NAME, result.taskResult.getUserInfo().getGivenName()); intent.putExtra(AuthenticationConstants.Broker.ACCOUNT_USERINFO_FAMILY_NAME, result.taskResult.getUserInfo().getFamilyName()); intent.putExtra(AuthenticationConstants.Broker.ACCOUNT_USERINFO_IDENTITY_PROVIDER, result.taskResult.getUserInfo().getIdentityProvider()); intent.putExtra(AuthenticationConstants.Broker.ACCOUNT_USERINFO_USERID_DISPLAYABLE, result.taskResult.getUserInfo().getDisplayableId()); } returnResult(AuthenticationConstants.UIResponse.TOKEN_BROKER_RESPONSE, intent); } else { returnError(ADALError.AUTHORIZATION_CODE_NOT_EXCHANGED_FOR_TOKEN, result.taskException.getMessage()); } } } class TokenTaskResult { AuthenticationResult taskResult; Exception taskException; String accountName; } }