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.URL; import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.crypto.NoSuchPaddingException; import com.microsoft.aad.adal.AuthenticationRequest.UserIdentifierType; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Bundle; import android.os.Handler; import android.support.v4.content.LocalBroadcastManager; import android.util.SparseArray; /** * ADAL context to get access token, refresh token, and lookup from cache. */ public class AuthenticationContext { private static final int EXCLUDE_INDEX = 8; private static final String TAG = "AuthenticationContext"; private Context mContext; private String mAuthority; private boolean mValidateAuthority; private boolean mAuthorityValidated = false; private ITokenCacheStore mTokenCacheStore; private static final ReentrantReadWriteLock RWL = new ReentrantReadWriteLock(); private static final Lock READ_LOCK = RWL.readLock(); private static final Lock WRITE_LOCK = RWL.writeLock(); /** * Delegate map is needed to handle activity recreate without asking * developer to handle context instance for config changes. */ static SparseArray<AuthenticationRequestState> mDelegateMap = new SparseArray<AuthenticationRequestState>(); /** * Last set authorization callback. */ private AuthenticationCallback<AuthenticationResult> mAuthorizationCallback; /** * Instance validation related calls are serviced inside Discovery as a * module. */ private IDiscovery mDiscovery = new Discovery(); /** * Web request handler interface to test behaviors. */ private IWebRequestHandler mWebRequest = new WebRequestHandler(); /** * JWS message builder interface to test behaviors. */ private IJWSBuilder mJWSBuilder; /** * Connection service interface to test different behaviors. */ private IConnectionService mConnectionService = null; private IBrokerProxy mBrokerProxy = null; /** * CorrelationId set by user or generated by ADAL. */ private UUID mRequestCorrelationId = null; /** * Constructs context to use with known authority to get the token. It uses * default cache that stores encrypted tokens. * * @param appContext It needs to have handle to the {@link Context} to use * the SharedPreferences as a Default cache storage. It does not * need to be activity. * @param authority Authority url to send code and token requests * @param validateAuthority validate authority before sending token request * @throws NoSuchPaddingException Algorithm padding does not exist in the * device * @throws NoSuchAlgorithmException Encryption Algorithm does not exist in * the device. Please see the log record for details. */ public AuthenticationContext(Context appContext, String authority, boolean validateAuthority) throws NoSuchAlgorithmException, NoSuchPaddingException { // Fixes are required for SDK 16-18 // The fixes need to be applied before any use of Java Cryptography // Architecture primitives. Default cache uses encryption PRNGFixes.apply(); initialize(appContext, authority, new DefaultTokenCacheStore(appContext), validateAuthority, true); } /** * Constructs context to use with known authority to get the token. It uses * provided cache. * * @param appContext {@link Context} * @param authority Authority Url * @param validateAuthority true/false for validation * @param tokenCacheStore Set to null if you don't want cache. */ public AuthenticationContext(Context appContext, String authority, boolean validateAuthority, ITokenCacheStore tokenCacheStore) { initialize(appContext, authority, tokenCacheStore, validateAuthority, false); } /** * It will verify the authority and use the given cache. If cache is null, * it will not use cache. * * @param appContext {@link Context} * @param authority Authority Url * @param tokenCacheStore Cache {@link ITokenCacheStore} used to store * tokens. Set to null if you don't want cache. */ public AuthenticationContext(Context appContext, String authority, ITokenCacheStore tokenCacheStore) { initialize(appContext, authority, tokenCacheStore, true, false); } private void initialize(Context appContext, String authority, ITokenCacheStore tokenCacheStore, boolean validateAuthority, boolean defaultCache) { if (appContext == null) { throw new IllegalArgumentException("appContext"); } if (authority == null) { throw new IllegalArgumentException("authority"); } mBrokerProxy = new BrokerProxy(appContext); if (!defaultCache && !mBrokerProxy.canUseLocalCache()) { throw new UnsupportedOperationException("Local cache is not supported for broker usage"); } mContext = appContext; mConnectionService = new DefaultConnectionService(mContext); checkInternetPermission(); mAuthority = extractAuthority(authority); mValidateAuthority = validateAuthority; mTokenCacheStore = tokenCacheStore; mJWSBuilder = new JWSBuilder(); } /** * Returns referenced cache. You can use default cache, which uses * SharedPreferences and handles synchronization by itself. * * @return ITokenCacheStore Current cache used */ public ITokenCacheStore getCache() { if (mBrokerProxy.canSwitchToBroker()) { // return cache implementation related to broker so that app can // clear tokens for related accounts return new ITokenCacheStore() { /** * default serial # */ private static final long serialVersionUID = 1L; @Override public void setItem(String key, TokenCacheItem item) { throw new UnsupportedOperationException( "Broker cache does not support direct setItem operation"); } @Override public void removeItem(String key) { throw new UnsupportedOperationException( "Broker cache does not support direct removeItem operation"); } @Override public void removeAll() { mBrokerProxy.removeAccounts(); } @Override public TokenCacheItem getItem(String key) { throw new UnsupportedOperationException( "Broker cache does not support direct getItem operation"); } @Override public boolean contains(String key) { throw new UnsupportedOperationException("Broker cache does not support contains operation"); } }; } return mTokenCacheStore; } /** * Gets authority that is used for this object of AuthenticationContext. * * @return Authority */ public String getAuthority() { return mAuthority; } /** * @return True when authority is valid */ public boolean getValidateAuthority() { return mValidateAuthority; } /** * Gets username for current broker user. * * @return Username */ public String getBrokerUser() { if (mBrokerProxy != null) { return mBrokerProxy.getCurrentUser(); } return null; } /* * Gets user info from broker. This should not be called on main thread. * @return user {@link UserInfo} * * @throws IOException * @throws AuthenticatorException * @throws OperationCanceledException */ public UserInfo[] getBrokerUsers() throws OperationCanceledException, AuthenticatorException, IOException { return mBrokerProxy != null ? mBrokerProxy.getBrokerUsers() : null; } /** * Get expected redirect Uri for your app to use in broker. You need to * register this redirectUri in order to get token from Broker. * * @return RedirectUri string to use for broker requests. */ public String getRedirectUriForBroker() { PackageHelper helper = new PackageHelper(mContext); String packageName = mContext.getPackageName(); // First available signature. Applications can be signed with multiple // signatures. String signatureDigest = helper.getCurrentSignatureForPackage(packageName); String redirectUri = PackageHelper.getBrokerRedirectUrl(packageName, signatureDigest); Logger.v(TAG, "Broker redirectUri:" + redirectUri + " packagename:" + packageName + " signatureDigest:" + signatureDigest); return redirectUri; } /** * acquire Token will start interactive flow if needed. It checks the cache * to return existing result if not expired. It tries to use refresh token * if available. If it fails to get token with refresh token, it will remove * this refresh token from cache and start authentication. * * @param activity required to launch authentication activity. * @param resource required resource identifier. * @param clientId required client identifier * @param redirectUri Optional. It will use package name info if not * provided. * @param loginHint Optional login hint * @param callback required */ public void acquireToken(Activity activity, String resource, String clientId, String redirectUri, String loginHint, AuthenticationCallback<AuthenticationResult> callback) { redirectUri = checkInputParameters(resource, clientId, redirectUri, PromptBehavior.Auto, callback); final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri, loginHint, PromptBehavior.Auto, null, getRequestCorrelationId()); request.setUserIdentifierType(UserIdentifierType.LoginHint); acquireTokenLocal(wrapActivity(activity), false, request, callback); } /** * acquire Token will start interactive flow if needed. It checks the cache * to return existing result if not expired. It tries to use the refresh * token if available. If it fails to get token with refresh token, it will * remove this refresh token from cache and fall back on the UI. * * @param activity Calling activity * @param resource required resource identifier. * @param clientId required client identifier * @param redirectUri Optional. It will use packagename and provided suffix * for this. * @param loginHint Optional. This parameter will be used to pre-populate * the username field in the authentication form. Please note * that the end user can still edit the username field and * authenticate as a different user. This parameter can be null. * @param extraQueryParameters Optional. This parameter will be appended as * is to the query string in the HTTP authentication request to * the authority. The parameter can be null. * @param callback required {@link AuthenticationCallback} object for async * call. */ public void acquireToken(Activity activity, String resource, String clientId, String redirectUri, String loginHint, String extraQueryParameters, AuthenticationCallback<AuthenticationResult> callback) { redirectUri = checkInputParameters(resource, clientId, redirectUri, PromptBehavior.Auto, callback); final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri, loginHint, PromptBehavior.Auto, extraQueryParameters, getRequestCorrelationId()); request.setUserIdentifierType(UserIdentifierType.LoginHint); acquireTokenLocal(wrapActivity(activity), false, request, callback); } /** * acquire Token will start interactive flow if needed. It checks the cache * to return existing result if not expired. It tries to use refresh token * if available. If it fails to get token with refresh token, behavior will * depend on options. If {@link PromptBehavior} is AUTO, it will remove this * refresh token from cache and fall back on the UI. Default is AUTO. if * {@link PromptBehavior} is Always, it will display prompt screen. * * @param activity Calling activity * @param resource required resource identifier. * @param clientId required client identifier. * @param redirectUri Optional. It will use packagename and provided suffix * for this. * @param prompt Optional. {@link PromptBehavior} added as query parameter * to authorization url * @param callback required {@link AuthenticationCallback} object for async * call. */ public void acquireToken(Activity activity, String resource, String clientId, String redirectUri, PromptBehavior prompt, AuthenticationCallback<AuthenticationResult> callback) { redirectUri = checkInputParameters(resource, clientId, redirectUri, prompt, callback); final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri, null, prompt, null, getRequestCorrelationId()); acquireTokenLocal(wrapActivity(activity), false, request, callback); } /** * acquire Token will start interactive flow if needed. It checks the cache * to return existing result if not expired. It tries to use refresh token * if available. If it fails to get token with refresh token, behavior will * depend on options. If promptbehavior is AUTO, it will remove this refresh * token from cache and fall back on the UI if activitycontext is not null. * Default is AUTO. * * @param activity Calling activity * @param resource required resource identifier. * @param clientId required client identifier. * @param redirectUri Optional. It will use packagename and provided suffix * for this. * @param prompt Optional. added as query parameter to authorization url * @param extraQueryParameters Optional. added to authorization url * @param callback required {@link AuthenticationCallback} object for async * call. */ public void acquireToken(Activity activity, String resource, String clientId, String redirectUri, PromptBehavior prompt, String extraQueryParameters, AuthenticationCallback<AuthenticationResult> callback) { redirectUri = checkInputParameters(resource, clientId, redirectUri, prompt, callback); final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri, null, prompt, extraQueryParameters, getRequestCorrelationId()); acquireTokenLocal(wrapActivity(activity), false, request, callback); } /** * acquire Token will start interactive flow if needed. It checks the cache * to return existing result if not expired. It tries to use refresh token * if available. If it fails to get token with refresh token, behavior will * depend on options. If promptbehavior is AUTO, it will remove this refresh * token from cache and fall back on the UI if activitycontext is not null. * Default is AUTO. * * @param activity Calling activity * @param resource required resource identifier. * @param clientId required client identifier. * @param redirectUri Optional. It will use packagename and provided suffix * for this. * @param loginHint Optional. It is used for cache and as a loginhint at * authentication. * @param prompt Optional. added as query parameter to authorization url * @param extraQueryParameters Optional. added to authorization url * @param callback required {@link AuthenticationCallback} object for async * call. */ public void acquireToken(Activity activity, String resource, String clientId, String redirectUri, String loginHint, PromptBehavior prompt, String extraQueryParameters, AuthenticationCallback<AuthenticationResult> callback) { redirectUri = checkInputParameters(resource, clientId, redirectUri, prompt, callback); final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri, loginHint, prompt, extraQueryParameters, getRequestCorrelationId()); request.setUserIdentifierType(UserIdentifierType.LoginHint); acquireTokenLocal(wrapActivity(activity), false, request, callback); } /** * It will start interactive flow if needed. It checks the cache to return * existing result if not expired. It tries to use refresh token if * available. If it fails to get token with refresh token, behavior will * depend on options. If promptbehavior is AUTO, it will remove this refresh * token from cache and fall back on the UI. Default is AUTO. * * @param fragment It accepts both type of fragments. * @param resource required resource identifier. * @param clientId required client identifier. * @param redirectUri Optional. It will use packagename and provided suffix * for this. * @param loginHint Optional. It is used for cache and as a loginhint at * authentication. * @param prompt Optional. added as query parameter to authorization url * @param extraQueryParameters Optional. added to authorization url * @param callback required {@link AuthenticationCallback} object for async * call. */ public void acquireToken(IWindowComponent fragment, String resource, String clientId, String redirectUri, String loginHint, PromptBehavior prompt, String extraQueryParameters, AuthenticationCallback<AuthenticationResult> callback) { redirectUri = checkInputParameters(resource, clientId, redirectUri, prompt, callback); final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri, loginHint, prompt, extraQueryParameters, getRequestCorrelationId()); request.setUserIdentifierType(UserIdentifierType.LoginHint); acquireTokenLocal(fragment, false, request, callback); } /** * This uses new dialog based prompt. It will create a handler to run the * dialog related code. It will start interactive flow if needed. It checks * the cache to return existing result if not expired. It tries to use * refresh token if available. If it fails to get token with refresh token, * behavior will depend on options. If promptbehavior is AUTO, it will * remove this refresh token from cache and fall back on the UI. Default is * AUTO. * * @param resource required resource identifier. * @param clientId required client identifier. * @param redirectUri Optional. It will use packagename and provided suffix * for this. * @param loginHint Optional. It is used for cache and as a loginhint at * authentication. * @param prompt Optional. added as query parameter to authorization url * @param extraQueryParameters Optional. added to authorization url * @param callback required {@link AuthenticationCallback} object for async * call. */ public void acquireToken(String resource, String clientId, String redirectUri, String loginHint, PromptBehavior prompt, String extraQueryParameters, AuthenticationCallback<AuthenticationResult> callback) { redirectUri = checkInputParameters(resource, clientId, redirectUri, prompt, callback); final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri, loginHint, prompt, extraQueryParameters, getRequestCorrelationId()); request.setUserIdentifierType(UserIdentifierType.LoginHint); acquireTokenLocal(null, true, request, callback); } private IWindowComponent wrapActivity(final Activity activity) { return new IWindowComponent() { Activity refActivity = activity; @Override public void startActivityForResult(Intent intent, int requestCode) { refActivity.startActivityForResult(intent, requestCode); } }; } private String checkInputParameters(String resource, String clientId, String redirectUri, PromptBehavior behavior, AuthenticationCallback<AuthenticationResult> callback) { if (mContext == null) { throw new AuthenticationException(ADALError.DEVELOPER_CONTEXT_IS_NOT_PROVIDED); } if (StringExtensions.IsNullOrBlank(resource)) { throw new IllegalArgumentException("resource"); } if (StringExtensions.IsNullOrBlank(clientId)) { throw new IllegalArgumentException("clientId"); } if (callback == null) { throw new IllegalArgumentException("callback"); } if (StringExtensions.IsNullOrBlank(redirectUri)) { redirectUri = getRedirectFromPackage(); } return redirectUri; } /** * This is sync function. It will first look at the cache and automatically * checks for the token expiration. Additionally, if no suitable access * token is found in the cache, but refresh token is available, the function * will use the refresh token automatically. This method will not show UI * for the user. If prompt is needed, the method will return an exception * * @param resource required resource identifier. * @param clientId required client identifier. * @param userId UserID obtained from * {@link AuthenticationResult #getUserInfo()} * @return A {@link Future} object representing the * {@link AuthenticationResult} of the call. It contains Access * Token,the Access Token's expiration time, Refresh token, and * {@link UserInfo}. */ public AuthenticationResult acquireTokenSilentSync(String resource, String clientId, String userId) { Future<AuthenticationResult> futureResult = acquireTokenSilent(resource, clientId, userId, null); try { return futureResult.get(); } catch (InterruptedException e) { convertExceptionForSync(e); } catch (ExecutionException e) { convertExceptionForSync(e); } return null; } private void convertExceptionForSync(Exception e) { // change to unchecked exception if (e.getCause() != null) { if (e.getCause() instanceof AuthenticationException) { throw (AuthenticationException) e.getCause(); } else if (e.getCause() instanceof IllegalArgumentException) { throw (IllegalArgumentException) e.getCause(); } else { throw new AuthenticationException(ADALError.ERROR_SILENT_REQUEST, e.getCause().getMessage(), e.getCause()); } } throw new AuthenticationException(ADALError.ERROR_SILENT_REQUEST, e.getMessage(), e); } /** * The function will first look at the cache and automatically checks for * the token expiration. Additionally, if no suitable access token is found * in the cache, but refresh token is available, the function will use the * refresh token automatically. This method will not show UI for the user. * If prompt is needed, the method will return an exception * * @param resource required resource identifier. * @param clientId required client identifier. * @param userId UserId obtained from {@link UserInfo} inside * {@link AuthenticationResult} * @param callback required {@link AuthenticationCallback} object for async * call. * @return A {@link Future} object representing the * {@link AuthenticationResult} of the call. It contains Access * Token,the Access Token's expiration time, Refresh token, and * {@link UserInfo}. */ public Future<AuthenticationResult> acquireTokenSilent(String resource, String clientId, String userId, AuthenticationCallback<AuthenticationResult> callback) { if (StringExtensions.IsNullOrBlank(resource)) { throw new IllegalArgumentException("resource"); } if (StringExtensions.IsNullOrBlank(clientId)) { throw new IllegalArgumentException("clientId"); } final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, userId, getRequestCorrelationId()); request.setSilent(true); request.setPrompt(PromptBehavior.Auto); request.setUserIdentifierType(UserIdentifierType.UniqueId); return acquireTokenLocal(null, false, request, callback); } /** * acquire token using refresh token if cache is not used. Otherwise, use * acquireToken to let the ADAL handle the cache lookup and refresh token * request. * * @param refreshToken Required. * @param clientId Required. * @param callback Required */ public void acquireTokenByRefreshToken(String refreshToken, String clientId, AuthenticationCallback<AuthenticationResult> callback) { // Authenticator is not supported if user is managing the cache refreshTokenWithoutCache(refreshToken, clientId, null, callback); } /** * acquire token using refresh token if cache is not used. Otherwise, use * acquireToken to let the ADAL handle the cache lookup and refresh token * request. * * @param refreshToken Required. * @param clientId Required. * @param resource Required resource identifier. * @param callback Required */ public void acquireTokenByRefreshToken(String refreshToken, String clientId, String resource, AuthenticationCallback<AuthenticationResult> callback) { // Authenticator is not supported if user is managing the cache refreshTokenWithoutCache(refreshToken, clientId, resource, callback); } /** * This method wraps the implementation for onActivityResult at the related * Activity class. This method is called at UI thread. * * @param requestCode Request code provided at the start of the activity. * @param resultCode Result code set from the activity. * @param data {@link Intent} */ public void onActivityResult(int requestCode, int resultCode, Intent data) { // This is called at UI thread when Activity sets result back. // ResultCode is set back from AuthenticationActivity. RequestCode is // set when we start the activity for result. if (requestCode == AuthenticationConstants.UIRequest.BROWSER_FLOW) { if (data == null) { // If data is null, RequestId is unknown. It could not find // callback to respond to this request. Logger.e(TAG, "onActivityResult BROWSER_FLOW data is null.", "", ADALError.ON_ACTIVITY_RESULT_INTENT_NULL); } else { Bundle extras = data.getExtras(); final int requestId = extras.getInt(AuthenticationConstants.Browser.REQUEST_ID); final AuthenticationRequestState waitingRequest = getWaitingRequest(requestId); if (waitingRequest != null) { Logger.v(TAG, "onActivityResult RequestId:" + requestId); } else { Logger.e(TAG, "onActivityResult did not find waiting request for RequestId:" + requestId, "", ADALError.ON_ACTIVITY_RESULT_INTENT_NULL); // There is no matching callback to send error return; } // Cancel or browser error can use recorded request to figure // out original correlationId send with request. String correlationInfo = getCorrelationInfoFromWaitingRequest(waitingRequest); if (resultCode == AuthenticationConstants.UIResponse.TOKEN_BROKER_RESPONSE) { String accessToken = data.getStringExtra(AuthenticationConstants.Broker.ACCOUNT_ACCESS_TOKEN); String accountName = data.getStringExtra(AuthenticationConstants.Broker.ACCOUNT_NAME); mBrokerProxy.saveAccount(accountName); long expireTime = data.getLongExtra(AuthenticationConstants.Broker.ACCOUNT_EXPIREDATE, 0); Date expire = new Date(expireTime); UserInfo userinfo = UserInfo.getUserInfoFromBrokerResult(data.getExtras()); AuthenticationResult brokerResult = new AuthenticationResult(accessToken, null, expire, false, userinfo, "", ""); if (brokerResult != null && brokerResult.getAccessToken() != null) { waitingRequest.mDelagete.onSuccess(brokerResult); return; } } else if (resultCode == AuthenticationConstants.UIResponse.BROWSER_CODE_CANCEL) { // User cancelled the flow by clicking back button or // activating another activity Logger.v(TAG, "User cancelled the flow RequestId:" + requestId + correlationInfo); waitingRequestOnError(waitingRequest, requestId, new AuthenticationCancelError( "User cancelled the flow RequestId:" + requestId + correlationInfo)); } else if (resultCode == AuthenticationConstants.UIResponse.BROWSER_CODE_AUTHENTICATION_EXCEPTION) { Serializable authException = extras .getSerializable(AuthenticationConstants.Browser.RESPONSE_AUTHENTICATION_EXCEPTION); if (authException != null && authException instanceof AuthenticationException) { AuthenticationException exception = (AuthenticationException) authException; Logger.w(TAG, "Webview returned exception", exception.getMessage(), ADALError.WEBVIEW_RETURNED_AUTHENTICATION_EXCEPTION); waitingRequestOnError(waitingRequest, requestId, exception); } else { waitingRequestOnError(waitingRequest, requestId, new AuthenticationException( ADALError.WEBVIEW_RETURNED_INVALID_AUTHENTICATION_EXCEPTION)); } } else if (resultCode == AuthenticationConstants.UIResponse.BROWSER_CODE_ERROR) { String errCode = extras.getString(AuthenticationConstants.Browser.RESPONSE_ERROR_CODE); String errMessage = extras.getString(AuthenticationConstants.Browser.RESPONSE_ERROR_MESSAGE); Logger.v(TAG, "Error info:" + errCode + " " + errMessage + " for requestId: " + requestId + correlationInfo); waitingRequestOnError(waitingRequest, requestId, new AuthenticationException( ADALError.SERVER_INVALID_REQUEST, errCode + " " + errMessage)); } else if (resultCode == AuthenticationConstants.UIResponse.BROWSER_CODE_COMPLETE) { final AuthenticationRequest authenticationRequest = (AuthenticationRequest) extras .getSerializable(AuthenticationConstants.Browser.RESPONSE_REQUEST_INFO); final String endingUrl = extras.getString(AuthenticationConstants.Browser.RESPONSE_FINAL_URL); if (endingUrl.isEmpty()) { AuthenticationException e = new AuthenticationException( ADALError.WEBVIEW_RETURNED_EMPTY_REDIRECT_URL, "Webview did not reach the redirectUrl. " + authenticationRequest.getLogInfo()); Logger.e(TAG, e.getMessage(), "", e.getCode()); waitingRequestOnError(waitingRequest, requestId, e); } else { // Browser has the url and it will exchange auth code // for token final CallbackHandler callbackHandle = new CallbackHandler(mHandler, waitingRequest.mDelagete); // Executes all the calls inside the Runnable to return // immediately to // UI thread. All UI // related actions will be performed using the Handler. sThreadExecutor.submit(new Runnable() { @Override public void run() { Logger.v(TAG, "Processing url for token. " + authenticationRequest.getLogInfo()); Oauth2 oauthRequest = new Oauth2(authenticationRequest, mWebRequest); AuthenticationResult result = null; try { result = oauthRequest.getToken(endingUrl); Logger.v(TAG, "OnActivityResult processed the result. " + authenticationRequest.getLogInfo()); } catch (Exception exc) { String msg = "Error in processing code to get token. " + authenticationRequest.getLogInfo(); Logger.e(TAG, msg, ExceptionExtensions.getExceptionMessage(exc), ADALError.AUTHORIZATION_CODE_NOT_EXCHANGED_FOR_TOKEN, exc); // Call error at UI thread waitingRequestOnError(callbackHandle, waitingRequest, requestId, new AuthenticationException( ADALError.AUTHORIZATION_CODE_NOT_EXCHANGED_FOR_TOKEN, msg, exc)); return; } try { if (result != null) { Logger.v(TAG, "OnActivityResult is setting the token to cache. " + authenticationRequest.getLogInfo()); if (!StringExtensions.IsNullOrBlank(result.getAccessToken())) { setItemToCache(authenticationRequest, result, true); } if (waitingRequest != null && waitingRequest.mDelagete != null) { Logger.v(TAG, "Sending result to callback. " + authenticationRequest.getLogInfo()); callbackHandle.onSuccess(result); } } else { callbackHandle.onError(new AuthenticationException( ADALError.AUTHORIZATION_CODE_NOT_EXCHANGED_FOR_TOKEN)); } } finally { removeWaitingRequest(requestId); } } }); } } } } } private static boolean isUserMisMatch(final AuthenticationRequest request, final AuthenticationResult result) { if (result.getUserInfo() != null && !StringExtensions.IsNullOrBlank(result.getUserInfo().getUserId()) && !StringExtensions.IsNullOrBlank(request.getUserId())) { // Verify if IdToken is present and userid is specified return !request.getUserId().equalsIgnoreCase(result.getUserInfo().getUserId()); } // it should verify loginhint as well if specified if (result.getUserInfo() != null && !StringExtensions.IsNullOrBlank(result.getUserInfo().getDisplayableId()) && !StringExtensions.IsNullOrBlank(request.getLoginHint())) { // Verify if IdToken is present and userid is specified return !request.getLoginHint().equalsIgnoreCase(result.getUserInfo().getDisplayableId()); } return false; } /** * If request has correlationID, ADAL should report that instead of current * CorrelationId. * * @param waitingRequest * @return */ private String getCorrelationInfoFromWaitingRequest(final AuthenticationRequestState waitingRequest) { UUID requestCorrelationID = getRequestCorrelationId(); if (waitingRequest.mRequest != null) { requestCorrelationID = waitingRequest.mRequest.getCorrelationId(); } String correlationInfo = String.format(" CorrelationId: %s", requestCorrelationID.toString()); return correlationInfo; } private void waitingRequestOnError(final AuthenticationRequestState waitingRequest, int requestId, AuthenticationException exc) { if (waitingRequest != null && waitingRequest.mDelagete != null) { Logger.v(TAG, "Sending error to callback" + getCorrelationInfoFromWaitingRequest(waitingRequest)); waitingRequest.mDelagete.onError(exc); } if (exc != null && exc.getCode() != ADALError.AUTH_FAILED_CANCELLED) { removeWaitingRequest(requestId); } } private void waitingRequestOnError(CallbackHandler handler, final AuthenticationRequestState waitingRequest, int requestId, final AuthenticationException exc) { if (waitingRequest != null && waitingRequest.mDelagete != null) { Logger.v(TAG, "Sending error to callback" + getCorrelationInfoFromWaitingRequest(waitingRequest)); handler.onError(exc); } if (exc != null && exc.getCode() != ADALError.AUTH_FAILED_CANCELLED) { removeWaitingRequest(requestId); } } private void removeWaitingRequest(int requestId) { Logger.v(TAG, "Remove waiting request: " + requestId); WRITE_LOCK.lock(); try { mDelegateMap.remove(requestId); } finally { WRITE_LOCK.unlock(); } } private AuthenticationRequestState getWaitingRequest(int requestId) { Logger.v(TAG, "Get waiting request: " + requestId); AuthenticationRequestState request = null; READ_LOCK.lock(); try { request = mDelegateMap.get(requestId); } finally { READ_LOCK.unlock(); } if (request == null && mAuthorizationCallback != null && requestId == mAuthorizationCallback.hashCode()) { // it does not have the caller callback. It will check the last // callback if set Logger.e(TAG, "Request callback is not available for requestid:" + requestId + ". It will use last callback.", "", ADALError.CALLBACK_IS_NOT_FOUND); request = new AuthenticationRequestState(0, null, mAuthorizationCallback); } return request; } private void putWaitingRequest(int requestId, AuthenticationRequestState requestState) { Logger.v(TAG, "Put waiting request: " + requestId + getCorrelationInfoFromWaitingRequest(requestState)); if (requestState != null) { WRITE_LOCK.lock(); try { mDelegateMap.put(requestId, requestState); } finally { WRITE_LOCK.unlock(); } } } /** * Active authentication activity can be cancelled if it exists. It may not * be cancelled if activity is not launched yet. RequestId is the hashcode * of your AuthenticationCallback. * * @param requestId Hash code value of your callback to cancel activity * launch * @return true: if there is a valid waiting request and cancel message send * successfully. false: Request does not exist or cancel message not * send */ public boolean cancelAuthenticationActivity(int requestId) { AuthenticationRequestState request = getWaitingRequest(requestId); if (request == null || request.mDelagete == null) { // there is not any waiting callback Logger.v(TAG, "Current callback is empty. There is not any active authentication."); return true; } String currentCorrelationInfo = getCorrelationInfoFromWaitingRequest(request); Logger.v(TAG, "Current callback is not empty. There is an active authentication Activity." + currentCorrelationInfo); // intent to cancel. Authentication activity registers for this message // at onCreate event. final Intent intent = new Intent(AuthenticationConstants.Browser.ACTION_CANCEL); final Bundle extras = new Bundle(); intent.putExtras(extras); intent.putExtra(AuthenticationConstants.Browser.REQUEST_ID, requestId); // send intent to cancel any active authentication activity. // it may not cancel it, if activity takes some time to launch. boolean cancelResult = LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent); if (cancelResult) { // clear callback if broadcast message was successful Logger.v(TAG, "Cancel broadcast message was successful." + currentCorrelationInfo); request.mCancelled = true; request.mDelagete.onError(new AuthenticationCancelError("Cancel broadcast message was successful.")); } else { // Activity is not launched yet or receiver is not registered Logger.w(TAG, "Cancel broadcast message was not successful." + currentCorrelationInfo, "", ADALError.BROADCAST_CANCEL_NOT_SUCCESSFUL); } return cancelResult; } /** * Singled threaded Executor for async work. */ private static ExecutorService sThreadExecutor = Executors.newSingleThreadExecutor(); private Handler mHandler; class CallbackHandler { private Handler mRefHandler; private AuthenticationCallback<AuthenticationResult> callback; public CallbackHandler(Handler ref, AuthenticationCallback<AuthenticationResult> callbackExt) { mRefHandler = ref; callback = callbackExt; } public void onError(final AuthenticationException e) { if (mRefHandler != null && callback != null) { mRefHandler.post(new Runnable() { @Override public void run() { callback.onError(e); return; } }); } else { throw e; } } public void onSuccess(final AuthenticationResult result) { if (mRefHandler != null && callback != null) { mRefHandler.post(new Runnable() { @Override public void run() { callback.onSuccess(result); return; } }); } } } private Future<AuthenticationResult> acquireTokenLocal(final IWindowComponent activity, final boolean useDialog, final AuthenticationRequest request, final AuthenticationCallback<AuthenticationResult> externalCall) { getHandler(); final CallbackHandler callbackHandle = new CallbackHandler(mHandler, externalCall); // Executes all the calls inside the Runnable to return immediately to // user. All UI // related actions will be performed using Handler. Logger.setCorrelationId(getRequestCorrelationId()); Logger.v(TAG, "Sending async task from thread:" + android.os.Process.myTid()); return sThreadExecutor.submit(new Callable<AuthenticationResult>() { @Override public AuthenticationResult call() { Logger.v(TAG, "Running task in thread:" + android.os.Process.myTid()); return acquireTokenLocalCall(callbackHandle, activity, useDialog, request); } }); } /** * Only gets token from activity defined in this package. * * @param activity * @param request * @param prompt * @param callback * @return */ private AuthenticationResult acquireTokenLocalCall(final CallbackHandler callbackHandle, final IWindowComponent activity, final boolean useDialog, final AuthenticationRequest request) { URL authorityUrl = StringExtensions.getUrl(mAuthority); if (authorityUrl == null) { callbackHandle.onError(new AuthenticationException(ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_URL)); return null; } if (mValidateAuthority && !mAuthorityValidated) { try { final URL authorityUrlInCallback = authorityUrl; // Discovery call creates an Async Task to send // Web Requests // using a handler boolean result = validateAuthority(authorityUrl); if (result) { mAuthorityValidated = true; Logger.v(TAG, "Authority is validated: " + authorityUrlInCallback.toString()); } else { Logger.v(TAG, "Call external callback since instance is invalid" + authorityUrlInCallback.toString()); callbackHandle.onError( new AuthenticationException(ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_INSTANCE)); return null; } } catch (Exception exc) { Logger.e(TAG, "Authority validation has an error.", "", ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_INSTANCE, exc); callbackHandle .onError(new AuthenticationException(ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_INSTANCE)); return null; } } // Validated the authority or skipped the validation return acquireTokenAfterValidation(callbackHandle, activity, useDialog, request); } private boolean promptUser(PromptBehavior prompt) { return prompt == PromptBehavior.Always || prompt == PromptBehavior.REFRESH_SESSION; } private AuthenticationResult acquireTokenAfterValidation(CallbackHandler callbackHandle, final IWindowComponent activity, final boolean useDialog, final AuthenticationRequest request) { Logger.v(TAG, "Token request started"); // BROKER flow intercepts here // cache and refresh call happens through the authenticator service if (mBrokerProxy.canSwitchToBroker()) { Logger.v(TAG, "It switched to broker for context: " + mContext.getPackageName()); AuthenticationResult result = null; request.setVersion(getVersionName()); // Don't send background request, if prompt flag is always or // refresh_session if (!promptUser(request.getPrompt())) { try { result = mBrokerProxy.getAuthTokenInBackground(request); } catch (AuthenticationException ex) { // pass back to caller for known exceptions such as failure // to encrypt if (callbackHandle.callback != null) { callbackHandle.onError(ex); return null; } else { throw ex; } } } if (result != null && result.getAccessToken() != null && !result.getAccessToken().isEmpty()) { Logger.v(TAG, "Token is returned from background call "); if (callbackHandle.callback != null) { callbackHandle.onSuccess(result); } return result; } // Launch broker activity // if cache and refresh request is not handled. // Initial request to authenticator needs to launch activity to // record calling uid for the account. This happens for Prompt auto // or always behavior. if (!request.isSilent() && callbackHandle.callback != null && activity != null) { // Only happens with callback since silent call does not show UI Logger.v(TAG, "Launch activity for Authenticator"); mAuthorizationCallback = callbackHandle.callback; request.setRequestId(callbackHandle.callback.hashCode()); Logger.v(TAG, "Starting Authentication Activity with callback:" + callbackHandle.callback.hashCode()); putWaitingRequest(callbackHandle.callback.hashCode(), new AuthenticationRequestState( callbackHandle.callback.hashCode(), request, callbackHandle.callback)); if (result != null && result.isInitialRequest()) { Logger.v(TAG, "Initial request to authenticator"); // Record the initial request but not force a prompt } // onActivityResult will receive the response // Activity needs to launch to record calling app for this // account Intent brokerIntent = mBrokerProxy.getIntentForBrokerActivity(request); if (brokerIntent != null) { try { Logger.v(TAG, "Calling activity pid:" + android.os.Process.myPid() + " tid:" + android.os.Process.myTid() + "uid:" + android.os.Process.myUid()); activity.startActivityForResult(brokerIntent, AuthenticationConstants.UIRequest.BROWSER_FLOW); } catch (ActivityNotFoundException e) { Logger.e(TAG, "Activity login is not found after resolving intent", "", ADALError.DEVELOPER_ACTIVITY_IS_NOT_RESOLVED, e); callbackHandle .onError(new AuthenticationException(ADALError.BROKER_ACTIVITY_IS_NOT_RESOLVED)); } } else { callbackHandle .onError(new AuthenticationException(ADALError.DEVELOPER_ACTIVITY_IS_NOT_RESOLVED)); } } else { // User does not want to launch activity String msg = "Prompt is not allowed and failed to get token:"; Logger.e(TAG, msg, "", ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED); callbackHandle.onError( new AuthenticationException(ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED, msg)); } // It will start activity if callback is provided. Return null here. return null; } else { return localFlow(callbackHandle, activity, useDialog, request); } } private AuthenticationResult localFlow(CallbackHandler callbackHandle, final IWindowComponent activity, final boolean useDialog, final AuthenticationRequest request) { // Lookup access token from cache AuthenticationResult cachedItem = getItemFromCache(request); if (cachedItem != null && isUserMisMatch(request, cachedItem)) { if (callbackHandle.callback != null) { callbackHandle.onError(new AuthenticationException(ADALError.AUTH_FAILED_USER_MISMATCH)); return null; } else { throw new AuthenticationException(ADALError.AUTH_FAILED_USER_MISMATCH); } } if (!promptUser(request.getPrompt()) && isValidCache(cachedItem)) { Logger.v(TAG, "Token is returned from cache"); if (callbackHandle.callback != null) { callbackHandle.onSuccess(cachedItem); } return cachedItem; } Logger.v(TAG, "Checking refresh tokens"); RefreshItem refreshItem = getRefreshToken(request); if (!promptUser(request.getPrompt()) && refreshItem != null && !StringExtensions.IsNullOrBlank(refreshItem.mRefreshToken)) { Logger.v(TAG, "Refresh token is available and it will attempt to refresh token"); return refreshToken(callbackHandle, activity, useDialog, request, refreshItem, true); } else { Logger.v(TAG, "Refresh token is not available"); if (!request.isSilent() && callbackHandle.callback != null && (activity != null || useDialog)) { // start activity if other options are not available // delegate map is used to remember callback if another // instance of authenticationContext is created for config // change or similar at client app. mAuthorizationCallback = callbackHandle.callback; request.setRequestId(callbackHandle.callback.hashCode()); Logger.v(TAG, "Starting Authentication Activity with callback:" + callbackHandle.callback.hashCode()); putWaitingRequest(callbackHandle.callback.hashCode(), new AuthenticationRequestState( callbackHandle.callback.hashCode(), request, callbackHandle.callback)); if (useDialog) { AuthenticationDialog dialog = new AuthenticationDialog(mHandler, mContext, this, request); dialog.show(); } else { // onActivityResult will receive the response if (!startAuthenticationActivity(activity, request)) { callbackHandle .onError(new AuthenticationException(ADALError.DEVELOPER_ACTIVITY_IS_NOT_RESOLVED)); } } } else { // User does not want to launch activity Logger.e(TAG, "Prompt is not allowed and failed to get token:", "", ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED); callbackHandle .onError(new AuthenticationException(ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED)); } } return null; } protected boolean isRefreshable(AuthenticationResult cachedItem) { return cachedItem != null && !StringExtensions.IsNullOrBlank(cachedItem.getRefreshToken()); } private boolean isValidCache(AuthenticationResult cachedItem) { if (cachedItem != null && !StringExtensions.IsNullOrBlank(cachedItem.getAccessToken()) && !cachedItem.isExpired()) { return true; } return false; } /** * get token from cache to return it, if not expired. * * @param request * @return AuthenticationResult */ private AuthenticationResult getItemFromCache(final AuthenticationRequest request) { if (mTokenCacheStore != null) { // get token if resourceid matches to cache key. TokenCacheItem item = null; if (request.getUserIdentifierType() == UserIdentifierType.LoginHint) { item = mTokenCacheStore.getItem(CacheKey.createCacheKey(request, request.getLoginHint())); } if (request.getUserIdentifierType() == UserIdentifierType.UniqueId) { item = mTokenCacheStore.getItem(CacheKey.createCacheKey(request, request.getUserId())); } if (request.getUserIdentifierType() == UserIdentifierType.NoUser) { item = mTokenCacheStore.getItem(CacheKey.createCacheKey(request, null)); } if (item != null) { Logger.v(TAG, "getItemFromCache accessTokenId:" + getTokenHash(item.getAccessToken()) + " refreshTokenId:" + getTokenHash(item.getRefreshToken())); return AuthenticationResult.createResult(item); } } return null; } private String getTokenHash(String token) { try { return StringExtensions.createHash(token); } catch (NoSuchAlgorithmException e) { Logger.e(TAG, "Digest error", "", ADALError.DEVICE_NO_SUCH_ALGORITHM, e); } catch (UnsupportedEncodingException e) { Logger.e(TAG, "Digest error", "", ADALError.ENCODING_IS_NOT_SUPPORTED, e); } return ""; } /** * If refresh token fails, this needs to be removed from cache to not use * this again for next try. Error in refreshToken call will result in * another call to acquireToken. It may try multi resource refresh token for * second attempt. */ private class RefreshItem { String mRefreshToken; String mKey; boolean mMultiResource; UserInfo mUserInfo; String mRawIdToken; String mKeyWithUserId; String mKeyWithDisplayableId; public RefreshItem(String keyInCache, AuthenticationRequest request, TokenCacheItem item, boolean multiResource) { mKey = keyInCache; mMultiResource = multiResource; if (item != null) { mRefreshToken = item.getRefreshToken(); mUserInfo = item.getUserInfo(); mRawIdToken = item.getRawIdToken(); if (item.getUserInfo() != null) { mKeyWithUserId = CacheKey.createCacheKey(request, item.getUserInfo().getUserId()); mKeyWithDisplayableId = CacheKey.createCacheKey(request, item.getUserInfo().getDisplayableId()); } } } public RefreshItem(String refreshToken) { mMultiResource = false; mRefreshToken = refreshToken; } } private RefreshItem getRefreshToken(final AuthenticationRequest request) { RefreshItem refreshItem = null; if (mTokenCacheStore != null) { boolean multiResource = false; // target refreshToken for this resource first. CacheKey will // include the resourceId in the cachekey Logger.v(TAG, "Looking for regular refresh token"); String userId = request.getUserId(); if (StringExtensions.IsNullOrBlank(userId)) { // acquireTokenSilent expects userid field from UserInfo userId = request.getLoginHint(); } String keyUsed = CacheKey.createCacheKey(request, userId); TokenCacheItem item = mTokenCacheStore.getItem(keyUsed); if (item == null || StringExtensions.IsNullOrBlank(item.getRefreshToken())) { // if not present, check multiResource item in cache. Cache key // will not include resourceId in the cache key string. Logger.v(TAG, "Looking for Multi Resource Refresh token"); keyUsed = CacheKey.createMultiResourceRefreshTokenKey(request, userId); item = mTokenCacheStore.getItem(keyUsed); multiResource = true; } if (item != null && !StringExtensions.IsNullOrBlank(item.getRefreshToken())) { String refreshTokenHash = getTokenHash(item.getRefreshToken()); Logger.v(TAG, "Refresh token is available and id:" + refreshTokenHash + " Key used:" + keyUsed); refreshItem = new RefreshItem(keyUsed, request, item, multiResource); } } return refreshItem; } private void setItemToCache(final AuthenticationRequest request, AuthenticationResult result, boolean afterPrompt) throws AuthenticationException { if (mTokenCacheStore != null) { // User can ask for token without login hint. Next call from same // method should use token from cache. Logger.v(TAG, "Setting item to cache"); // Calculate token hashcode logReturnedToken(request, result); // acquireTokenSilent uses userid to request items String userKey = request.getUserId(); if (afterPrompt) { // User can change the username and enter a different one at // prompt. Use idtoken if present instead of loginhint after // prompt. if (result.getUserInfo() != null && !StringExtensions.IsNullOrBlank(result.getUserInfo().getDisplayableId())) { Logger.v(TAG, "Updating cache for username:" + result.getUserInfo().getDisplayableId()); setItemToCacheForUser(request, result, result.getUserInfo().getDisplayableId()); } } else if (StringExtensions.IsNullOrBlank(userKey)) { userKey = request.getLoginHint(); } // It will store in the cache for empty idtokens as well setItemToCacheForUser(request, result, userKey); // Set item with userid if idtoken is present. if (result.getUserInfo() != null && !StringExtensions.IsNullOrBlank(result.getUserInfo().getUserId())) { Logger.v(TAG, "Updating userId:" + result.getUserInfo().getUserId()); setItemToCacheForUser(request, result, result.getUserInfo().getUserId()); } } } private void setItemToCacheForUser(final AuthenticationRequest request, AuthenticationResult result, String userId) { mTokenCacheStore.setItem(CacheKey.createCacheKey(request, userId), new TokenCacheItem(request, result, false)); // Store broad refresh token if available if (result.getIsMultiResourceRefreshToken()) { Logger.v(TAG, "Setting Multi Resource Refresh token to cache"); mTokenCacheStore.setItem(CacheKey.createMultiResourceRefreshTokenKey(request, userId), new TokenCacheItem(request, result, true)); } } /** * Calculate hash for accessToken and log that. * * @param request * @param result */ private void logReturnedToken(final AuthenticationRequest request, final AuthenticationResult result) { if (result != null && result.getAccessToken() != null) { String accessTokenHash = getTokenHash(result.getAccessToken()); String refreshTokenHash = getTokenHash(result.getRefreshToken()); Logger.v(TAG, String.format("Access TokenID %s and Refresh TokenID %s returned. CorrelationId: %s", accessTokenHash, refreshTokenHash, request.getCorrelationId())); } } private void setItemToCacheFromRefresh(final RefreshItem refreshItem, final AuthenticationRequest request, AuthenticationResult result) throws AuthenticationException { if (mTokenCacheStore != null) { // Use same key to store refreshed result. This key may belong to // normal token or MRRT token. Logger.v(TAG, "Setting refresh item to cache for key:" + refreshItem.mKey); logReturnedToken(request, result); // Update for cache key mTokenCacheStore.setItem(refreshItem.mKey, new TokenCacheItem(request, result, refreshItem.mMultiResource)); setItemToCache(request, result, false); } } private void removeItemFromCache(final RefreshItem refreshItem) throws AuthenticationException { if (mTokenCacheStore != null) { Logger.v(TAG, "Remove refresh item from cache:" + refreshItem.mKey); mTokenCacheStore.removeItem(refreshItem.mKey); // clean up keys related to userid/displayableid for same request mTokenCacheStore.removeItem(refreshItem.mKeyWithUserId); mTokenCacheStore.removeItem(refreshItem.mKeyWithDisplayableId); } } /** * refresh token if possible. if it fails, it calls acquire token after * removing refresh token from cache. * * @param callbackHandle * @param activity Activity to use in case refresh token does not succeed * and prompt is not set to never. * @param request incoming request * @param refreshItem refresh item info to remove this refresh token from * cache * @param useCache refresh request can be explicit without cache usage. * Error message should return without trying prompt. * @param externalCallback * @return */ private AuthenticationResult refreshToken(final CallbackHandler callbackHandle, final IWindowComponent activity, final boolean useDialog, final AuthenticationRequest request, final RefreshItem refreshItem, final boolean useCache) { Logger.v(TAG, "Process refreshToken for " + request.getLogInfo() + " refreshTokenId:" + getTokenHash(refreshItem.mRefreshToken)); // Removes refresh token from cache, when this call is complete. Request // may be interrupted, if app is shutdown by user. Detect connection // state to not remove refresh token if user turned Airplane mode or // similar. if (!mConnectionService.isConnectionAvailable()) { AuthenticationException exc = new AuthenticationException(ADALError.DEVICE_CONNECTION_IS_NOT_AVAILABLE, "Connection is not available to refresh token"); Logger.w(TAG, "Connection is not available to refresh token", request.getLogInfo(), ADALError.DEVICE_CONNECTION_IS_NOT_AVAILABLE); callbackHandle.onError(exc); return null; } AuthenticationResult result = null; try { Oauth2 oauthRequest = new Oauth2(request, mWebRequest, mJWSBuilder); result = oauthRequest.refreshToken(refreshItem.mRefreshToken); if (result != null && StringExtensions.IsNullOrBlank(result.getRefreshToken())) { Logger.v(TAG, "Refresh token is not returned or empty"); result.setRefreshToken(refreshItem.mRefreshToken); } } catch (Exception exc) { // Server side error or similar Logger.e(TAG, "Error in refresh token for request:" + request.getLogInfo(), ExceptionExtensions.getExceptionMessage(exc), ADALError.AUTH_FAILED_NO_TOKEN, exc); AuthenticationException authException = new AuthenticationException(ADALError.AUTH_FAILED_NO_TOKEN, ExceptionExtensions.getExceptionMessage(exc), exc); callbackHandle.onError(authException); return null; } if (useCache) { if (result == null || StringExtensions.IsNullOrBlank(result.getAccessToken())) { String errLogInfo = result == null ? "" : result.getErrorLogInfo(); Logger.w(TAG, "Refresh token did not return accesstoken.", request.getLogInfo() + errLogInfo, ADALError.AUTH_FAILED_NO_TOKEN); // remove item from cache to avoid same usage of // refresh token in next acquireToken call removeItemFromCache(refreshItem); return acquireTokenLocalCall(callbackHandle, activity, useDialog, request); } else { Logger.v(TAG, "It finished refresh token request:" + request.getLogInfo()); if (result.getUserInfo() == null && refreshItem.mUserInfo != null) { Logger.v(TAG, "UserInfo is updated from cached result:" + request.getLogInfo()); result.setUserInfo(refreshItem.mUserInfo); result.setIdToken(refreshItem.mRawIdToken); } // it replaces multi resource refresh token as // well with the new one since it is not stored // with resource. Logger.v(TAG, "Cache is used. It will set item to cache" + request.getLogInfo()); setItemToCacheFromRefresh(refreshItem, request, result); // return result obj which has error code and // error description that is returned from // server response if (callbackHandle.callback != null) { callbackHandle.onSuccess(result); } return result; } } else { // User is not using cache and explicitly // calling with refresh token. User should received // error code and error description in // Authentication result for Oauth errors Logger.v(TAG, "Cache is not used for Request:" + request.getLogInfo()); if (callbackHandle.callback != null) { callbackHandle.onSuccess(result); } return result; } } private boolean validateAuthority(final URL authorityUrl) { // This is not calling outer callback. It is using // authenticationCallback, so handler is not needed here if (mDiscovery != null) { Logger.v(TAG, "Start validating authority"); // Set CorrelationId for Instance Discovery mDiscovery.setCorrelationId(getRequestCorrelationId()); try { boolean result = mDiscovery.isValidAuthority(authorityUrl); Logger.v(TAG, "Finish validating authority:" + authorityUrl + " result:" + result); return result; } catch (Exception exc) { Logger.e(TAG, "Instance validation returned error", "", ADALError.DEVELOPER_AUTHORITY_CAN_NOT_BE_VALIDED, exc); } } return false; } private String getRedirectFromPackage() { return mContext.getApplicationContext().getPackageName(); } /** * @param activity * @param request * @return false: if intent is not resolved or error in starting. true: if * intent is sent to start the activity. */ private boolean startAuthenticationActivity(final IWindowComponent activity, AuthenticationRequest request) { Intent intent = getAuthenticationActivityIntent(activity, request); if (!resolveIntent(intent)) { Logger.e(TAG, "Intent is not resolved", "", ADALError.DEVELOPER_ACTIVITY_IS_NOT_RESOLVED); return false; } try { // Start activity from callers context so that caller can intercept // when it is done activity.startActivityForResult(intent, AuthenticationConstants.UIRequest.BROWSER_FLOW); } catch (ActivityNotFoundException e) { Logger.e(TAG, "Activity login is not found after resolving intent", "", ADALError.DEVELOPER_ACTIVITY_IS_NOT_RESOLVED, e); return false; } return true; } /** * Resolve activity from the package. If developer did not declare the * activity, it will not resolve. * * @param intent * @return true if activity is defined in the package. */ private final boolean resolveIntent(Intent intent) { ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivity(intent, 0); if (resolveInfo == null) { return false; } return true; } /** * Get intent to start authentication activity. * * @param request * @return intent for authentication activity */ private final Intent getAuthenticationActivityIntent(IWindowComponent activity, AuthenticationRequest request) { Intent intent = new Intent(); if (AuthenticationSettings.INSTANCE.getActivityPackageName() != null) { // This will use the activity from another given package. intent.setClassName(AuthenticationSettings.INSTANCE.getActivityPackageName(), AuthenticationActivity.class.getName()); } else { // This will lookup the authentication activity within this context intent.setClass(mContext, AuthenticationActivity.class); } intent.putExtra(AuthenticationConstants.Browser.REQUEST_MESSAGE, request); return intent; } /** * Get the CorrelationId set by user. * * @return UUID */ public UUID getRequestCorrelationId() { if (mRequestCorrelationId == null) { return UUID.randomUUID(); } return mRequestCorrelationId; } /** * set CorrelationId to requests. * * @param mRequestCorrelationId */ public void setRequestCorrelationId(UUID requestCorrelationId) { this.mRequestCorrelationId = requestCorrelationId; Logger.setCorrelationId(requestCorrelationId); } /** * Developer is using refresh token call to do refresh without cache usage. * App context or activity is not needed. Async requests are created,so this * needs to be called at UI thread. */ private void refreshTokenWithoutCache(final String refreshToken, final String clientId, final String resource, final AuthenticationCallback<AuthenticationResult> externalCallback) { Logger.setCorrelationId(getRequestCorrelationId()); Logger.v(TAG, "Refresh token without cache"); if (StringExtensions.IsNullOrBlank(refreshToken)) { throw new IllegalArgumentException("Refresh token is not provided"); } if (StringExtensions.IsNullOrBlank(clientId)) { throw new IllegalArgumentException("ClientId is not provided"); } if (externalCallback == null) { throw new IllegalArgumentException("Callback is not provided"); } final CallbackHandler callbackHandle = new CallbackHandler(getHandler(), externalCallback); // Execute all the calls inside Runnable to return immediately. All UI // related actions will be performed using Handler. sThreadExecutor.submit(new Runnable() { @Override public void run() { final URL authorityUrl = StringExtensions.getUrl(mAuthority); if (authorityUrl == null) { callbackHandle .onError(new AuthenticationException(ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_URL)); return; } final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, getRequestCorrelationId()); // It is not using cache and refresh is not expected to // show authentication activity. request.setSilent(true); final RefreshItem refreshItem = new RefreshItem(refreshToken); if (mValidateAuthority) { Logger.v(TAG, "Validating authority"); try { if (validateAuthority(authorityUrl)) { Logger.v(TAG, "Authority is validated" + authorityUrl.toString()); } else { Logger.v(TAG, "Call callback since instance is invalid:" + authorityUrl.toString()); callbackHandle.onError(new AuthenticationException( ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_INSTANCE)); return; } } catch (Exception exc) { Logger.e(TAG, "Authority validation is failed", ExceptionExtensions.getExceptionMessage(exc), ADALError.SERVER_INVALID_REQUEST, exc); callbackHandle.onError(new AuthenticationException(ADALError.SERVER_INVALID_REQUEST, "Authority validation is failed")); return; } } // Follow refresh logic now. Authority is valid or // skipped validation refreshToken(callbackHandle, null, false, request, refreshItem, false); } }); } private synchronized Handler getHandler() { if (mHandler == null) { // Use current main looper mHandler = new Handler(mContext.getMainLooper()); } return mHandler; } private static String extractAuthority(String authority) { if (!StringExtensions.IsNullOrBlank(authority)) { // excluding the starting https:// or http:// int thirdSlash = authority.indexOf("/", EXCLUDE_INDEX); // third slash is not the last character if (thirdSlash >= 0 && thirdSlash != (authority.length() - 1)) { int fourthSlash = authority.indexOf("/", thirdSlash + 1); if (fourthSlash < 0 || fourthSlash > thirdSlash + 1) { if (fourthSlash >= 0) { return authority.substring(0, fourthSlash); } return authority; } } } throw new IllegalArgumentException("authority"); } private void checkInternetPermission() { PackageManager pm = mContext.getPackageManager(); if (PackageManager.PERMISSION_GRANTED != pm.checkPermission("android.permission.INTERNET", mContext.getPackageName())) { throw new AuthenticationException(ADALError.DEVELOPER_INTERNET_PERMISSION_MISSING); } } class DefaultConnectionService implements IConnectionService { private Context mConnectionContext; DefaultConnectionService(Context ctx) { mConnectionContext = ctx; } public boolean isConnectionAvailable() { ConnectivityManager connectivityManager = (ConnectivityManager) mConnectionContext .getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo(); boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting(); return isConnected; } } /** * Version name for ADAL not for the app itself. * * @return Version */ public static String getVersionName() { // Package manager does not report for ADAL // AndroidManifest files are not merged, so it is returning hard coded // value return "1.1.1"; } }