diff --git a/.travis.yml b/.travis.yml index bc3204525..a402fbdf9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,14 @@ # ./travis.yml for adal android language: android +dist: precise jdk: - oraclejdk8 +# Don't use the Travis Container-Based Infrastructure +sudo: true + android: components: # Travis has a bug that we need to workaround to use sdk 25 @@ -17,14 +21,35 @@ android: - extra - extra-android-m2repository - extra-google-m2repository + - addon-google_apis-google-21 + #system images + - sys-img-armeabi-v7a-addon-google_apis-google-21 env: + global: + # This is to guaratee a clean gradle log + - TERM=dumb matrix: - ANDROID_SDKS=android-25 ANDROID_TARGET=android-25 before_install: - chmod +x gradlew +before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ + - $HOME/.android/build-cache + +before_script: + - echo no | android create avd --force -n test -t android-21 --abi armeabi-v7a + - emulator -avd test -no-audio -no-window & + - android-wait-for-emulator + - adb shell input keyevent 82 & + script: - cd $PWD - - ./gradlew clean build --info + - ./gradlew clean build connectedAndroidTest -PdisablePreDex diff --git a/README.md b/README.md index 3158a465d..f418c46e6 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,18 @@ The ADAL SDK for Android gives you the ability to add support for Work Accounts A Work Account is an identity you use to get work done no matter if at your business or on a college campus. Anywhere you need to get access to your work life you'll use a Work Account. The Work Account can be tied to an Active Directory server running in your datacenter or live completely in the cloud like when you use Office365. A Work Account will be how your users know that they are accessing their important documents and data backed my Microsoft security. -## ADAL for Android 1.12.0 Released! +## ADAL for Android 1.12.1 Released! + +## Build status +| Branch | Status | +| ------------- | ------------- | +| dev (Travis) | [![Build Status](https://travis-ci.org/AzureAD/azure-activedirectory-library-for-android.svg?branch=master)](https://travis-ci.org/AzureAD/azure-activedirectory-library-for-android) | +| dev (VSTS) | [![Build status](https://identitydivision.visualstudio.com/_apis/public/build/definitions/a7934fdd-dcde-4492-a406-7fad6ac00e17/94/badge)](https://identitydivision.visualstudio.com/IDDP/_build/index?definitionId=94&_a=completed) | + +Note: A corpnet account is required to view the VSTS build. ## Versions -Current version - 1.12.0 +Current version - 1.12.1 Minimum recommended version - 1.1.16 You can find the changes for each version in the [change log](https://github.com/AzureAD/azure-activedirectory-library-for-android/blob/master/changelog.txt). @@ -35,6 +43,12 @@ We leverage [Stack Overflow](http://stackoverflow.com/) to work with the communi We recommend you use the "adal" tag so we can see it! Here is the latest Q&A on Stack Overflow for ADAL: [http://stackoverflow.com/questions/tagged/adal](http://stackoverflow.com/questions/tagged/adal) +## SSO and Conditional Access Support + +This library allows your application to support our [Enterprise Mobility Suite](https://www.microsoft.com/en-us/cloud-platform/enterprise-mobility-security), including [Conditional Access](https://www.microsoft.com/en-us/cloud-platform/conditional-access), so businesses can use your application in their secure environment. + +To configure your application to support these scenarios, please read this document: [How to enable cross-app SSO on Android using ADAL](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-sso-android) + ## Security Reporting If you find a security issue with our libraries or services please report it to [secure@microsoft.com](mailto:secure@microsoft.com) with as much detail as possible. Your submission may be eligible for a bounty through the [Microsoft Bounty](http://aka.ms/bugbounty) program. Please do not post security issues to GitHub Issues or any other public site. We will contact you shortly upon receiving the information. We encourage you to get notifications of when security incidents occur by visiting [this page](https://technet.microsoft.com/en-us/security/dd252948) and subscribing to Security Advisory Alerts. @@ -57,8 +71,6 @@ To build with Gradle, * You should see app 'hello' installed in the test device * Enter test user credentials to try - - To build with Maven, you can use the pom.xml at top level * Clone this repo in to a directory of your choice @@ -73,25 +85,25 @@ To build with Maven, you can use the pom.xml at top level Jar packages will be also submitted beside the aar package. -##Download +## Download We've made it easy for you to have multiple options to use this library in your Android project: * You can use the source code to import this library into Android Studio and link to your application. * If using Android Studio, you can use *aar* package format and reference the binaries. -###Option 1: Source Zip +### Option 1: Source Zip To download a copy of the source code, click "Download ZIP" on the right side of the page or click [here](https://github.com/AzureAD/azure-activedirectory-library-for-android/archive/v1.1.5.tar.gz). -###Option 2: Source via Git +### Option 2: Source via Git To get the source code of the SDK via git just type: git clone git@github.com:AzureAD/azure-activedirectory-library-for-android.git cd ./azure-activedirectory-library-for-android/src -###Option 3: Binaries via Gradle +### Option 3: Binaries via Gradle You can get the binaries from Maven central repo. AAR package can be included as follows in your project in AndroidStudio: @@ -101,7 +113,7 @@ repositories { } dependencies { // your dependencies here... - compile('com.microsoft.aad:adal:1.1.11') { + compile('com.microsoft.aad:adal:1.12.+') { // if your app includes android support // libraries or Gson in its dependencies // exclude that groupId from ADAL's compile @@ -114,7 +126,7 @@ dependencies { } ``` -###Option 4: aar via Maven +### Option 4: aar via Maven If you are using the m2e plugin in Eclipse, you can specify the dependency in your pom.xml file: @@ -122,12 +134,12 @@ If you are using the m2e plugin in Eclipse, you can specify the dependency in yo com.microsoft.aad adal - 1.1.11 + 1.12.0 aar ``` -###Option 5: jar package inside libs folder +### Option 5: jar package inside libs folder You can get the jar file from maven the repo and drop into the *libs* folder in your project. You need to copy the required resources to your project as well since the jar packages don't include them. ## Prerequisites @@ -149,34 +161,35 @@ You can get the jar file from maven the repo and drop into the *libs* folder in 4. Update your project's AndroidManifest.xml file to include: - ```Java - - - - - - - .... - - ``` +```xml + + + + + + +.... + +``` 5. Register your WEBAPI service app in Azure Active Directory (AAD). If you're not sure what a tenant is or how you would get one, read [What is a Microsoft Azure AD tenant](http://technet.microsoft.com/library/jj573650.aspx)? or [Sign up for Microsoft Azure as an organization](http://www.windowsazure.com/en-us/manage/services/identity/organizational-account/). These docs should get you started on your way to using Windows Azure AD. * NOTE: You need to write down the APP ID URI for the next steps 6. Register your client native app at AAD. Select webapis in the list and give permission to previously registered WebAPI. If you need help with this step, see: [Register the REST API Service Windows Azure Active Directory](https://github.com/AzureADSamples/WebAPI-Nodejs/wiki/Setup-Windows-Azure-AD) * NOTE: You will need to write down the clientId and redirectUri parameters for the next steps. 7. Create an instance of AuthenticationContext at your main Activity. The details of this call are beyond the scope of this README, but you can get a good start by looking at the [Android Native Client Sample](https://github.com/AzureADSamples/NativeClient-Android). Below is an example: - - ```Java + + ```java // Authority is in the form of https://login.windows.net/yourtenant.onmicrosoft.com mContext = new AuthenticationContext(MainActivity.this, authority, true); // This will use SharedPreferences as default cache ``` + * NOTE: mContext is a field in your activity 8. Copy this code block to handle the end of AuthenticationActivity after user enters credentials and receives authorization code: @@ -224,59 +237,57 @@ You can get the jar file from maven the repo and drop into the *libs* folder in } }; ``` + 10. Finally, ask for a token using that callback: ```Java mContext.acquireToken(MainActivity.this, resource, clientId, redirect, user_loginhint, PromptBehavior.Auto, "", callback); ``` -If you're implementing your authentication logic in a Fragment, you'll need to wrap it in a `IWindowComponent` before passing it as a parameter like this: + + If you're implementing your authentication logic in a Fragment, you'll need to wrap it in a `IWindowComponent` before passing it as a parameter like this: - ```Java - mContext.acquireToken(wrapFragment(MainFragment.this), resource, clientId, redirect, user_loginhint, PromptBehavior.Auto, "", +```java +mContext.acquireToken(wrapFragment(MainFragment.this), resource, clientId, redirect, user_loginhint, PromptBehavior.Auto, "", callback); - - private IWindowComponent wrapFragment(final Fragment fragment){ - return new IWindowComponent() { - Fragment refFragment = fragment; - @Override - public void startActivityForResult(Intent intent, int requestCode) { - refFragment.startActivityForResult(intent, requestCode); - } - }; - } - ``` - Explanation of the parameters(Example of those parameters could be found at [Android Native Client Sample](https://github.com/AzureADSamples/NativeClient-Android)): - - * Resource is required and is the resource you are trying to access. - * Clientid is required and comes from the AzureAD Portal. - * You can setup redirectUri as your packagename. It is not required to be provided for the acquireToken call. - * PromptBehavior helps to ask for credentials to skip cache and cookie. - * Callback will be called after authorization code is exchanged for a token. - - The Callback will have an object of AuthenticationResult which has accesstoken, date expired, and idtoken info. - - **acquireTokenSilentSync** +private IWindowComponent wrapFragment(final Fragment fragment){ + return new IWindowComponent() { + Fragment refFragment = fragment; + @Override + public void startActivityForResult(Intent intent, int requestCode) { + refFragment.startActivityForResult(intent, requestCode); + } + }; +} +``` - In order to get token back without prompt, you can call **acquireTokenSilentSync** which handles caching, and token refresh without UI prompt. It provides async version as well. **Note:** userId required in silent call is the one you get back from the interactive call) as parameter. - - ```java - mContext.acquireTokenSilentSync(String resource, String clientId, String userId); - ``` - or - ```java - mContext.acquireTokenSilent(String resource, String clientId, String userId, final AuthenticationCallback callback); - ``` + Explanation of the parameters(Example of those parameters could be found at [Android Native Client Sample](https://github.com/AzureADSamples/NativeClient-Android)): * Resource is required and is the resource you are trying to access. + * Clientid is required and comes from the AzureAD Portal. + * You can setup redirectUri as your packagename. It is not required to be provided for the acquireToken call. + * PromptBehavior helps to ask for credentials to skip cache and cookie. + * Callback will be called after authorization code is exchanged for a token. + + The Callback will have an object of AuthenticationResult which has accesstoken, date expired, and idtoken info. + +**acquireTokenSilentSync** + +In order to get token back without prompt, you can call **acquireTokenSilentSync** which handles caching, and token refresh without UI prompt. It provides async version as well. **Note:** userId required in silent call is the one you get back from the interactive call) as parameter. + +```java +mContext.acquireTokenSilentSync(String resource, String clientId, String userId); +``` +or +```java +mContext.acquireTokenSilent(String resource, String clientId, String userId, final AuthenticationCallback callback); +``` 11. Broker: Microsoft Intune's Company portal App and Azure Authenticator App will provide the broker component. In order to acquire token via broker, the following requirements have to meet(Please check samples\userappwithbroker for authentication via broker): * Starting version 1.1.14, developer has to explicitly specify set to use broker via: - ``` - AuthenticationSettings.Instance.setUseBroker(true); - ``` + `AuthenticationSettings.INSTANCE.setUseBroker(true);` * Developer needs to register special redirectUri for broker usage. RedirectUri is in the format of msauth://packagename/Base64UrlencodedSignature. You can get your redirecturi for your app using the script `brokerRedirectPrint.ps1` on Windows or `brokerRedirectPrint.sh` on Linux or Mac. You can also use API call mContext.getBrokerRedirectUri. Signature is related to your signing certificates. * If target version is lower than 23, calling app has to have the following permissions declared in manifest(http://developer.android.com/reference/android/accounts/AccountManager.html): * GET_ACCOUNTS @@ -286,10 +297,9 @@ If you're implementing your authentication logic in a Fragment, you'll need to w * There must be an account existed and registered via one of the two broker apps. AuthenticationContext provides API method to get the broker user. - - ```java - String brokerAccount = mContext.getBrokerUser(); - ``` + + `String brokerAccount = mContext.getBrokerUser();` + Broker user will be returned if account is valid. Using this walkthrough, you should have what you need to successfully integrate with Azure Active Directory. For more examples of this working, visit the AzureADSamples/ repository on GitHub. @@ -317,11 +327,14 @@ Federated sign-in may fail when attempting to authenticate using the Azure Activ ### Querying cache items ADAL provides Default cache in SharedPrefrecens with some simple cache query fucntions. You can get the current cache from AuthenticationContext with: -```Java - ITokenCacheStore cache = mContext.getCache(); + +```java +ITokenCacheStore cache = mContext.getCache(); ``` + You can also provide your cache implementation, if you want to customize it. -```Java + +```java mContext = new AuthenticationContext(MainActivity.this, authority, true, yourCache); ``` @@ -333,7 +346,7 @@ ADAL provides option to specifiy prompt behavior. PromptBehavior.Auto will pop u This method does not use UI pop up and not require an activity. It will return token from cache if available. If token is expired, it will try to refresh it. If refresh token is expired or failed, it will return AuthenticationException. -```Java +```java Future result = mContext.acquireTokenSilent(resource, clientid, userId, callback ); ``` @@ -358,7 +371,7 @@ This is obviously the first diagnostic. We try to provide helpful error messages You can configure the library to generate log messages that you can use to help diagnose issues. You configure logging by making the following call to configure a callback that ADAL will use to hand off each log message as it is generated. - ```Java + ```java Logger.getInstance().setExternalLogger(new ILogger() { @Override public void Log(String tag, String message, String additionalMessage, LogLevel level, ADALError errorCode) { @@ -370,7 +383,7 @@ You can configure the library to generate log messages that you can use to help ``` Messages can be written to a custom log file as seen below. Unfortunately, there is no standard way of getting logs from a device. There are some services that can help you with this. You can also invent your own, such as sending the file to a server. -```Java +```java private syncronized void writeToLogFile(Context ctx, String msg) { File directory = ctx.getDir(ctx.getPackageName(), Context.MODE_PRIVATE); File logFile = new File(directory, "logfile"); @@ -390,18 +403,20 @@ private syncronized void writeToLogFile(Context ctx, String msg) { + Verbose(More details) You set the log level like this: -```Java -Logger.getInstance().setLogLevel(Logger.LogLevel.Verbose); - ``` +`Logger.getInstance().setLogLevel(Logger.LogLevel.Verbose);` All log messages are sent to logcat in addition to any custom log callbacks. - You can get log to a file form logcat as shown belog: + You can get log to a file form logcat as shown below: + `adb logcat > "C:\logmsg\logfile.txt"` - ``` - adb logcat > "C:\logmsg\logfile.txt" - ``` More examples about adb cmds: https://developer.android.com/tools/debugging/debugging-log.html#startingLogcat - + +#### Telemetry + +ADAL provides a built-in callback mechanism to supply consuming applications with event data (telemetry) generated during requests. The event data is sanitized of Personally Identifiable Information (PII) and Organizationally Identifiable Information (OII) and is designed to give consumers of the library insight into the performance, reliability, and usage of ADAL. + +For detailed guidance on the usage, configuration, and schema of ADAL telemetry, see [Wiki:Telemetry](https://github.com/AzureAD/azure-activedirectory-library-for-android/wiki/Telemetry) + #### Network Traces You can use various tools to capture the HTTP traffic that ADAL generates. This is most useful if you are familiar with the OAuth protocol or if you need to provide diagnostic information to Microsoft or other support channels. @@ -420,8 +435,9 @@ acquireToken method without activity supports dialog prompt. ADAL encrypts the tokens and store in SharedPreferences by default. You can look at the StorageHelper class to see the details. ADAL uses AndroidKeyStore for 4.3(API18) and above for secure storage of private keys. If you want to use ADAL for lower SDK versions, you need to **provide secret key at AuthenticationSettings.INSTANCE.setSecretKey** Following example is using the password based encryption key(which takes the specified password and salt). And then create the provider-independent secret key with the generated password based encryption key. ADAL requires the key to be 256 bits. You can use other key generation algorithm. -```Java - SecretKeyFactory keyFactory = SecretKeyFactory + +```java + SecretKeyFactory keyFactory = SecretKeyFactory .getInstance("PBEWithSHA256And256BitAES-CBC-BC"); SecretKey generatedSecretKey = keyFactory.generateSecret(new PBEKeySpec(your_password, byte-code-for-your-salt, 100, 256)); @@ -431,7 +447,7 @@ Following example is using the password based encryption key(which takes the spe ### Oauth2 Bearer challange -AuthenticationParameters class provides functionality to get the authorization_uri from Oauth2 bearer challange. +`AuthenticationParameters` class provides functionality to get the authorization_uri from Oauth2 bearer challange. ### Session cookies in Webview @@ -450,7 +466,7 @@ The ADAL library includes English strings for the following two ProgressDialog m Your application should overwrite them if localized strings are desired. -```Java +```xml Loading... Broker is processing Username diff --git a/RELEASES.md b/RELEASES.md index 7193de310..afb0df82f 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -2,7 +2,7 @@ We have adopted the semantic versioning flow that is industry standard for OSS projects. It gives the maximum amount of control on what risk you take with what versions. If you know how semantic versioning works with node.js, java, and ruby none of this will be new. -##Semantic Versioning and API stability promises +## Semantic Versioning and API stability promises Microsoft Identity libraries are independent open source libraries that are used by partners both internal and external to Microsoft. As with the rest of Microsoft, we have moved to a rapid iteration model where bugs are fixed daily and new versions are produced as required. To communicate these frequent changes to external partners and customers, we use semantic versioning for all our public Microsoft Identity SDK libraries. This follows the practices of other open source libraries on the internet. This allows us to support our downstream partners which will lock on certain versions for stability purposes, as well as providing for the distribution over NuGet, CocoaPods, and Maven. diff --git a/adal/build.gradle b/adal/build.gradle index df4c91ccb..5155fdd64 100644 --- a/adal/build.gradle +++ b/adal/build.gradle @@ -1,4 +1,6 @@ apply plugin: 'com.android.library' +// Add JaCoCo for coverage metrics +apply plugin: 'jacoco' // This plugin publishes adal in to the local maven repo apply plugin: 'com.github.dcendents.android-maven' apply plugin: 'findbugs' @@ -8,6 +10,10 @@ apply plugin: 'maven-publish' group = 'com.microsoft.aad' +configurations { + javadocDeps +} + buildscript { repositories { jcenter() @@ -25,12 +31,15 @@ android { minSdkVersion 14 targetSdkVersion 25 versionCode 1 - versionName "1.12.0" + versionName "1.13.0" project.archivesBaseName = "adal" project.version = android.defaultConfig.versionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { + debug { + testCoverageEnabled true + } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt') @@ -42,6 +51,8 @@ android { manifest.srcFile 'src/main/AndroidManifest.xml' java.srcDirs = ['src/main/java', 'src/main/aidl'] } + + androidTest.setRoot('src/androidTest') } sourceSets { @@ -78,7 +89,9 @@ dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) //Compile Dependencies compile 'com.android.support:appcompat-v7:25.0.0' - compile 'com.google.code.gson:gson:2.2.4' + compile 'com.google.code.gson:gson:2.8.0' + compile 'com.android.support:support-annotations:25.0.0' + compile 'com.android.support:support-v4:25.0.0' //Android Instrumental Test Dependencies androidTestCompile 'com.android.support.test:runner:0.5' @@ -90,8 +103,31 @@ dependencies { // Test Dependencies testCompile 'junit:junit:4.12' + + // Javadoc Dependencies + javadocDeps 'com.android.support:support-annotations:25.0.0' + javadocDeps 'com.android.support:support-v4:25.0.0' } +task jacocoTestReport(type: JacocoReport, dependsOn: 'testDebugUnitTest') { + reports { + xml.enabled = true + html.enabled = true + } + + jacocoClasspath = configurations['androidJacocoAnt'] + + def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*'] + def debugTree = fileTree(dir: "${buildDir}/intermediates/classes/debug", excludes: fileFilter) + def mainSrc = "${project.projectDir}/src/main/java" + + sourceDirectories = files([mainSrc]) + classDirectories = files([debugTree]) + executionData = fileTree(dir: "$buildDir", includes: [ + "jacoco/testDebugUnitTest.exec", + "outputs/code-coverage/connected/*coverage.ec" + ]) +} task createPom { pom { @@ -139,6 +175,9 @@ task sourcesJar(type: Jar) { task javadoc(type: Javadoc) { source = android.sourceSets.main.java.srcDirs + classpath += configurations.compile + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) + classpath += configurations.javadocDeps exclude '**/*.aidl' if (JavaVersion.current().isJava8Compatible()) { allprojects { @@ -147,9 +186,7 @@ task javadoc(type: Javadoc) { } } } - classpath = configurations.compile destinationDir = reporting.file("$project.buildDir/outputs/jar/javadoc/") - failOnError = false } task javadocJar(type: Jar, dependsOn: javadoc) { diff --git a/adal/gradle/wrapper/gradle-wrapper.properties b/adal/gradle/wrapper/gradle-wrapper.properties index 3883d4969..252376b97 100644 --- a/adal/gradle/wrapper/gradle-wrapper.properties +++ b/adal/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/adal/src/androidTest/AndroidManifest.xml b/adal/src/androidTest/AndroidManifest.xml index 7dd1063be..935b7b8e8 100644 --- a/adal/src/androidTest/AndroidManifest.xml +++ b/adal/src/androidTest/AndroidManifest.xml @@ -10,10 +10,6 @@ - - ) Matchers.eq(null), - Matchers.any(Handler.class)); - - // verify returned AT is as expected - assertNull(callback.getCallbackException()); - assertNotNull(callback.getCallbackResult()); - assertTrue(callback.getCallbackResult().getAccessToken().equals("I am an access token from broker")); - - //verify local cache is cleared - assertNull(cacheStore.getItem(CacheKey.createCacheKeyForRTEntry(VALID_AUTHORITY, "resource", "clientid", - TEST_USERID))); - assertNull(cacheStore.getItem(CacheKey.createCacheKeyForRTEntry(VALID_AUTHORITY, "resource", "clientid", - TEST_UPN))); - - assertNull(cacheStore.getItem(CacheKey.createCacheKeyForMRRT(VALID_AUTHORITY, "clientid", TEST_UPN))); - assertNull(cacheStore.getItem(CacheKey.createCacheKeyForMRRT(VALID_AUTHORITY, "clientid", TEST_USERID))); - - assertFalse(authContext.getCache().getAll().hasNext()); - cacheStore.removeAll(); + authContext.acquireTokenSilentAsync("resource", "clientid", TEST_USERID, new AuthenticationCallback() { + @Override + public void onSuccess(AuthenticationResult result) { + // verify getAuthToken called once + verify(mockedAccountManager, times(1)).getAuthToken(Mockito.any(Account.class), Matchers.anyString(), + Matchers.any(Bundle.class), Matchers.eq(false), (AccountManagerCallback) Matchers.eq(null), + Matchers.any(Handler.class)); + + assertNotNull(result); + assertTrue(result.getAccessToken().equals("I am an access token from broker")); + + //verify local cache is cleared + assertNull(cacheStore.getItem(CacheKey.createCacheKeyForRTEntry(VALID_AUTHORITY, "resource", "clientid", + TEST_USERID))); + assertNull(cacheStore.getItem(CacheKey.createCacheKeyForRTEntry(VALID_AUTHORITY, "resource", "clientid", + TEST_UPN))); + + assertNull(cacheStore.getItem(CacheKey.createCacheKeyForMRRT(VALID_AUTHORITY, "clientid", TEST_UPN))); + assertNull(cacheStore.getItem(CacheKey.createCacheKeyForMRRT(VALID_AUTHORITY, "clientid", TEST_USERID))); + + assertFalse(authContext.getCache().getAll().hasNext()); + cacheStore.removeAll(); + signal.countDown(); + } + + @Override + public void onError(Exception exc) { + fail(); + } + }); + + signal.await(); } /** @@ -333,11 +348,19 @@ public void testBothLocalAndBrokerSilentAuthFailedSwitchedToBrokerForInteractive VALID_AUTHORITY, false, cacheStore); final TestAuthCallback callback = new TestAuthCallback(); - authContext.acquireToken(Mockito.mock(Activity.class), "resource", "clientid", + final CountDownLatch latch = new CountDownLatch(1); + final Activity mockedActivity = Mockito.mock(Activity.class); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + latch.countDown(); + return null; + } + }).when(mockedActivity).startActivityForResult(Mockito.any(Intent.class), Mockito.anyInt()); + authContext.acquireToken(mockedActivity, "resource", "clientid", authContext.getRedirectUriForBroker(), TEST_UPN, callback); - final CountDownLatch signal = new CountDownLatch(1); - signal.await(ACTIVITY_TIME_OUT, TimeUnit.MILLISECONDS); + latch.await(); // verify getAuthToken called once verify(mockedAccountManager, times(1)).getAuthToken(Mockito.any(Account.class), Matchers.anyString(), @@ -397,6 +420,223 @@ public void testLocalSilentFailedBrokerSilentReturnErrorCannotTryWithInteractive cacheStore.removeAll(); } + @Test + public void testMultipleATExistForSameClientAppAndResource() throws PackageManager.NameNotFoundException, InterruptedException { + // insert multiple ATs for the same client app and resource + final ITokenCacheStore cacheStore = getTokenCache(getExpireDate(MINUS_MINUITE), false, false, null); + final String resource = "resource"; + final String clientId = "clientid"; + insertTokenForDifferentUser(clientId, resource, getExpireDate(MINUS_MINUITE), cacheStore); + + final FileMockContext mockContext = createMockContext(); + final AuthenticationContext authContext = new AuthenticationContext(mockContext, + VALID_AUTHORITY, false, cacheStore); + + final CountDownLatch latch = new CountDownLatch(1); + authContext.acquireTokenSilentAsync(resource, clientId, null, new AuthenticationCallback() { + @Override + public void onSuccess(AuthenticationResult result) { + fail("unexpected success"); + } + + @Override + public void onError(Exception exc) { + assertTrue(exc instanceof AuthenticationException); + final AuthenticationException authenticationException = (AuthenticationException) exc; + assertTrue(authenticationException.getCode().equals(ADALError.AUTH_FAILED_USER_MISMATCH)); + latch.countDown(); + } + }); + latch.await(); + } + + @Test + public void testMutipleMRRTExistForTheSameApp() throws PackageManager.NameNotFoundException, InterruptedException { + final ITokenCacheStore cacheStore = getTokenCache(getExpireDate(MINUS_MINUITE), true, false, null); + final String resource = "resource"; + final String clientId = "clientid"; + insertTokenForDifferentUser(clientId, resource, getExpireDate(MINUS_MINUITE), cacheStore); + + final FileMockContext mockContext = createMockContext(); + final AuthenticationContext authContext = new AuthenticationContext(mockContext, + VALID_AUTHORITY, false, cacheStore); + + final CountDownLatch latch = new CountDownLatch(1); + authContext.acquireTokenSilentAsync("different resource", clientId, null, new AuthenticationCallback() { + @Override + public void onSuccess(AuthenticationResult result) { + fail("unexpected success"); + } + + @Override + public void onError(Exception exc) { + assertTrue(exc instanceof AuthenticationException); + final AuthenticationException authenticationException = (AuthenticationException) exc; + assertTrue(authenticationException.getCode().equals(ADALError.AUTH_FAILED_USER_MISMATCH)); + latch.countDown(); + } + }); + latch.await(); + } + + private void insertTokenForDifferentUser(final String clientId, final String resource, final Date expiresOn, final ITokenCacheStore cacheStore) { + final String anotherUpn = "another_upn"; + final String anotherUserId = "another_userid"; + final UserInfo userInfo = new UserInfo(); + userInfo.setDisplayableId(anotherUpn); + userInfo.setUserId(anotherUserId); + final String idToken = "I am a different id token"; + + final AuthenticationResult result = new AuthenticationResult("different at", "different rt", expiresOn, false, userInfo, "", idToken, null); + final TokenCacheItem differentTokenItem = TokenCacheItem.createRegularTokenCacheItem(VALID_AUTHORITY, resource, clientId, result); + cacheStore.setItem(CacheKey.createCacheKeyForRTEntry(VALID_AUTHORITY, resource, clientId, anotherUserId), differentTokenItem); + cacheStore.setItem(CacheKey.createCacheKeyForRTEntry(VALID_AUTHORITY, resource, clientId, anotherUpn), differentTokenItem); + cacheStore.setItem(CacheKey.createCacheKeyForRTEntry(VALID_AUTHORITY, resource, clientId, null), differentTokenItem); + + final TokenCacheItem mrrtItem = TokenCacheItem.createMRRTTokenCacheItem(VALID_AUTHORITY, clientId, result); + cacheStore.setItem(CacheKey.createCacheKeyForMRRT(VALID_AUTHORITY, clientId, anotherUpn), mrrtItem); + cacheStore.setItem(CacheKey.createCacheKeyForMRRT(VALID_AUTHORITY, clientId, anotherUserId), mrrtItem); + cacheStore.setItem(CacheKey.createCacheKeyForMRRT(VALID_AUTHORITY, clientId, null), mrrtItem); + } + + public void testEmbeddedAuthCacheSkippedWhenClaimsSent() throws PackageManager.NameNotFoundException, IOException, InterruptedException { + // Make sure AT is not expired + final ITokenCacheStore cacheStore = getTokenCache(getExpireDate(MINUS_MINUITE), false, false, null); + final FileMockContext mockContext = createMockContext(); + final PackageManager packageManager = mockContext.getPackageManager(); + when(packageManager.resolveActivity(Mockito.any(Intent.class), Mockito.anyInt())).thenReturn(Mockito.mock(ResolveInfo.class)); + final HttpURLConnection mockedConnection = prepareFailedHttpUrlConnection("invalid_request"); + + final AuthenticationContext authContext = new AuthenticationContext(mockContext, + VALID_AUTHORITY, false, cacheStore); + + final TestAuthCallback callback = new TestAuthCallback(); + final CountDownLatch latch = new CountDownLatch(1); + final Activity mockedActivity = Mockito.mock(Activity.class); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + latch.countDown(); + return null; + } + }).when(mockedActivity).startActivityForResult(Mockito.any(Intent.class), Mockito.anyInt()); + + authContext.acquireToken(mockedActivity, "resource", "clientid", authContext.getRedirectUriForBroker(), TEST_UPN, PromptBehavior.Auto, null, "testClaims", callback); + latch.await(); + + Mockito.verifyZeroInteractions(mockedConnection); + } + + public void testEmbeddedAuthCacheNotSkippedClaimsSentInExtraQp() throws PackageManager.NameNotFoundException, IOException, InterruptedException { + // Make sure AT is not expired + final ITokenCacheStore cacheStore = getTokenCache(getExpireDate(MINUS_MINUITE), false, false, null); + final FileMockContext mockContext = createMockContext(); + final PackageManager packageManager = mockContext.getPackageManager(); + when(packageManager.resolveActivity(Mockito.any(Intent.class), Mockito.anyInt())).thenReturn(Mockito.mock(ResolveInfo.class)); + + final AuthenticationContext authContext = new AuthenticationContext(mockContext, + VALID_AUTHORITY, false, cacheStore); + + final CountDownLatch latch = new CountDownLatch(1); + authContext.acquireToken(Mockito.mock(Activity.class), "resource", "clientid", authContext.getRedirectUriForBroker(), TEST_UPN, + PromptBehavior.Auto, "claims=testclaims123", null, new AuthenticationCallback() { + @Override + public void onSuccess(AuthenticationResult result) { + assertNotNull(result.getAccessToken()); + latch.countDown(); + } + + @Override + public void onError(Exception exc) { + fail(); + } + }); + latch.await(); + } + + public void testClaimsSentInBothClaimParameterAndExtraQP() throws PackageManager.NameNotFoundException, IOException, + OperationCanceledException, AuthenticatorException, InterruptedException { + final ITokenCacheStore cacheStore = getTokenCache(getExpireDate(-MINUS_MINUITE), false, false, null); + + final AccountManager mockedAccountManager = getMockedAccountManager(); + mockAccountManagerGetAccountBehavior(mockedAccountManager); + mockGetAuthTokenCall(mockedAccountManager, false); + mockAddAccountCall(mockedAccountManager); + + final FileMockContext mockContext = createMockContext(); + mockContext.setMockedAccountManager(mockedAccountManager); + + final AuthenticationContext authContext = new AuthenticationContext(mockContext, + VALID_AUTHORITY, false, cacheStore); + + try { + authContext.acquireToken(Mockito.mock(Activity.class), "resource", "clientid", authContext.getRedirectUriForBroker(), + TEST_UPN, PromptBehavior.Auto, "claims=testClaims234", "testClaims123", new TestAuthCallback()); + fail("Expect exception to be thrown."); + } catch (final Exception e) { + assertTrue(e instanceof IllegalArgumentException); + } + + final IWindowComponent fragment = new IWindowComponent() { + @Override + public void startActivityForResult(Intent intent, int requestCode) { } + }; + + try { + authContext.acquireToken(fragment, "resource", "clientid", authContext.getRedirectUriForBroker(), + TEST_UPN, PromptBehavior.Auto, "claims=testClaims234", "testClaims123", new TestAuthCallback()); + fail("Expect exception to be thrown."); + } catch (final Exception e) { + assertTrue(e instanceof IllegalArgumentException); + } + + try { + authContext.acquireToken("resource", "clientid", authContext.getRedirectUriForBroker(), + TEST_UPN, PromptBehavior.Auto, "claims=testClaims234", "testClaims123", new TestAuthCallback()); + fail("Expect exception to be thrown."); + } catch (final Exception e) { + assertTrue(e instanceof IllegalArgumentException); + } + } + + public void testBrokerAuthCacheSkippedWhenClaimsSent() throws PackageManager.NameNotFoundException, IOException, + OperationCanceledException, AuthenticatorException, InterruptedException { + final ITokenCacheStore cacheStore = getTokenCache(getExpireDate(-MINUS_MINUITE), false, false, null); + + final AccountManager mockedAccountManager = getMockedAccountManager(); + mockAccountManagerGetAccountBehavior(mockedAccountManager); + mockGetAuthTokenCall(mockedAccountManager, false); + mockAddAccountCall(mockedAccountManager); + + final FileMockContext mockContext = createMockContext(); + mockContext.setMockedAccountManager(mockedAccountManager); + + final HttpURLConnection mockedConnection = prepareFailedHttpUrlConnection("invalid_request"); + prepareAuthForBrokerCall(); + + final AuthenticationContext authContext = new AuthenticationContext(mockContext, + VALID_AUTHORITY, false, cacheStore); + final TestAuthCallback callback = new TestAuthCallback(); + final CountDownLatch latch = new CountDownLatch(1); + final Activity mockedActivity = Mockito.mock(Activity.class); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + latch.countDown(); + return null; + } + }).when(mockedActivity).startActivityForResult(Mockito.any(Intent.class), Mockito.anyInt()); + + authContext.acquireToken(mockedActivity, "resource", "clientid", authContext.getRedirectUriForBroker(), TEST_UPN, PromptBehavior.Auto, "", "testClaims123", callback); + latch.await(); + + // make sure no getAuthToken call made and no request to token endpoint(If there is one, there will be interaction with mocked httpUrlConnection). + Mockito.verify(mockedAccountManager, never()).getAuthToken(Mockito.any(Account.class), Matchers.anyString(), + Matchers.any(Bundle.class), Matchers.eq(false), (AccountManagerCallback) Matchers.eq(null), + Matchers.any(Handler.class)); + Mockito.verifyZeroInteractions(mockedConnection); + } + private ITokenCacheStore getTokenCache(final Date expiresOn, final boolean storeMRRT, final boolean storeFRT, final Date extendedExpiresOn) { // prepare valid item in cache final UserInfo userInfo = new UserInfo(); @@ -425,11 +665,13 @@ private ITokenCacheStore getTokenCache(final Date expiresOn, final boolean store regularRTItem); cacheStore.setItem(CacheKey.createCacheKeyForRTEntry(VALID_AUTHORITY, resource, clientId, TEST_UPN), regularRTItem); + cacheStore.setItem(CacheKey.createCacheKeyForRTEntry(VALID_AUTHORITY, resource, clientId, null), regularRTItem); if (storeMRRT) { final TokenCacheItem mrrtItem = TokenCacheItem.createMRRTTokenCacheItem(VALID_AUTHORITY, clientId, result); cacheStore.setItem(CacheKey.createCacheKeyForMRRT(VALID_AUTHORITY, clientId, TEST_UPN), mrrtItem); cacheStore.setItem(CacheKey.createCacheKeyForMRRT(VALID_AUTHORITY, clientId, TEST_USERID), mrrtItem); + cacheStore.setItem(CacheKey.createCacheKeyForMRRT(VALID_AUTHORITY, clientId, null), mrrtItem); } if (storeFRT) { @@ -555,7 +797,7 @@ public void testVerifyBrokerRedirectUriInvalidSignature() * Test for returning a valid stale AT when ExtendedLifetime is on and the server is down. */ @SmallTest - public void testResiliencyTokenReturnExtendedLifetimeOnPositive() throws PackageManager.NameNotFoundException, + public void testResiliencyTokenReturnExtendedLifetimeOnMinServerError() throws PackageManager.NameNotFoundException, NoSuchAlgorithmException, OperationCanceledException, IOException, AuthenticatorException, InterruptedException { // make sure AT's expires_in is expired and ext_expires_in is not expired @@ -572,7 +814,41 @@ public void testResiliencyTokenReturnExtendedLifetimeOnPositive() throws Package Mockito.when(mockedConnection.getOutputStream()).thenReturn(Mockito.mock(OutputStream.class)); Mockito.when(mockedConnection.getInputStream()).thenReturn(Util.createInputStream(Util.getErrorResponseBody("HTTP_GATEWAY_TIMEOUT")), Util.createInputStream(Util.getErrorResponseBody("HTTP_GATEWAY_TIMEOUT"))); - Mockito.when(mockedConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_GATEWAY_TIMEOUT, HttpURLConnection.HTTP_GATEWAY_TIMEOUT); + Mockito.when(mockedConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR, HttpURLConnection.HTTP_INTERNAL_ERROR); + + try { + final AuthenticationResult result = authContext.acquireTokenSilentSync("resource", "clientid", TEST_USERID); + verify(mockedConnection, times(2)).getInputStream(); + assertNotNull(result); + assertTrue(result.getAccessToken().equals("I am an AT")); + assertTrue(result.isExtendedLifeTimeToken()); + assertNotNull(result.getExtendedExpiresOn()); + assertTrue(!TokenCacheItem.isTokenExpired(result.getExtendedExpiresOn())); + } catch (final AuthenticationException exception) { + fail("Did not expect an exception"); + } finally { + cacheStore.removeAll(); + } + } + + public void testResiliencyTokenReturnExtendedLifetimeOnMaxServerError() throws PackageManager.NameNotFoundException, + NoSuchAlgorithmException, OperationCanceledException, IOException, AuthenticatorException, + InterruptedException { + // make sure AT's expires_in is expired and ext_expires_in is not expired + final ITokenCacheStore cacheStore = getTokenCache(getExpireDate(-MINUS_MINUITE), false, false, getExpireDate(EXTEND_MINUS_MINUTE)); + + final FileMockContext mockContext = createMockContext(); + final AuthenticationContext authContext = new AuthenticationContext(mockContext, + VALID_AUTHORITY, false, cacheStore); + authContext.setExtendedLifetimeEnabled(true); + + final HttpURLConnection mockedConnection = Mockito.mock(HttpURLConnection.class); + HttpUrlConnectionFactory.setMockedHttpUrlConnection(mockedConnection); + Util.prepareMockedUrlConnection(mockedConnection); + Mockito.when(mockedConnection.getOutputStream()).thenReturn(Mockito.mock(OutputStream.class)); + Mockito.when(mockedConnection.getInputStream()).thenReturn(Util.createInputStream(Util.getErrorResponseBody("HTTP_GATEWAY_TIMEOUT")), + Util.createInputStream(Util.getErrorResponseBody("HTTP_GATEWAY_TIMEOUT"))); + Mockito.when(mockedConnection.getResponseCode()).thenReturn(MAX_RESILIENCY_ERROR_CODE, MAX_RESILIENCY_ERROR_CODE); try { final AuthenticationResult result = authContext.acquireTokenSilentSync("resource", "clientid", TEST_USERID); @@ -613,7 +889,7 @@ public void testResiliencyTokenReturnExtendedLifetimeOnwithRetryFail() throws Pa Mockito.when(mockedConnection.getOutputStream()).thenReturn(Mockito.mock(OutputStream.class)); Mockito.when(mockedConnection.getInputStream()).thenReturn(Util.createInputStream(Util.getErrorResponseBody("HTTP_GATEWAY_TIMEOUT")), Util.createInputStream(Util.getErrorResponseBody("HTTP_BAD_GATEWAY"))); - Mockito.when(mockedConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_GATEWAY_TIMEOUT, HttpURLConnection.HTTP_BAD_GATEWAY); + Mockito.when(mockedConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_GATEWAY_TIMEOUT, HttpURLConnection.HTTP_NOT_FOUND); try { authContext.acquireTokenSilentSync("resource", "clientid", TEST_USERID); @@ -765,8 +1041,8 @@ public void testResiliencyTokenReturnExtendedLifetimeOnwithoutRetry() throws Pac HttpUrlConnectionFactory.setMockedHttpUrlConnection(mockedConnection); Util.prepareMockedUrlConnection(mockedConnection); Mockito.when(mockedConnection.getOutputStream()).thenReturn(Mockito.mock(OutputStream.class)); - Mockito.when(mockedConnection.getInputStream()).thenReturn(Util.createInputStream(Util.getErrorResponseBody("HTTP_BAD_GATEWAY"))); - Mockito.when(mockedConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_BAD_GATEWAY); + Mockito.when(mockedConnection.getInputStream()).thenReturn(Util.createInputStream(Util.getErrorResponseBody("HTTP_CONFLICT"))); + Mockito.when(mockedConnection.getResponseCode()).thenReturn(MAX_RESILIENCY_ERROR_CODE + 1); //status code 600 try { authContext.acquireTokenSilentSync("resource", "clientid", TEST_USERID); @@ -1057,7 +1333,7 @@ private void prepareSuccessHttpUrlConnection() throws IOException { Mockito.when(mockedConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); } - private void prepareFailedHttpUrlConnection(final String errorCode, final String ...errorCodes) throws IOException { + private HttpURLConnection prepareFailedHttpUrlConnection(final String errorCode, final String ...errorCodes) throws IOException { final HttpURLConnection mockedConnection = Mockito.mock(HttpURLConnection.class); HttpUrlConnectionFactory.setMockedHttpUrlConnection(mockedConnection); Util.prepareMockedUrlConnection(mockedConnection); @@ -1067,6 +1343,8 @@ private void prepareFailedHttpUrlConnection(final String errorCode, final String Util.createInputStream(Util.getErrorResponseBody(errorCode)), errorCodes.length == 1 ? Util.createInputStream(Util.getErrorResponseBody(errorCodes[0])) : null); Mockito.when(mockedConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_BAD_REQUEST); + + return mockedConnection; } private String getEncodedTestingSignature() throws NoSuchAlgorithmException { diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/AndroidTestHelper.java b/adal/src/androidTest/java/com/microsoft/aad/adal/AndroidTestHelper.java index 52bd6c88f..072cab973 100644 --- a/adal/src/androidTest/java/com/microsoft/aad/adal/AndroidTestHelper.java +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/AndroidTestHelper.java @@ -24,6 +24,7 @@ package com.microsoft.aad.adal; import android.annotation.SuppressLint; +import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; @@ -56,8 +57,9 @@ protected void setUp() throws Exception { System.setProperty("dexmaker.dexcache", getInstrumentation().getTargetContext().getCacheDir().getPath()); // ADAL is set to this signature for now - PackageInfo info = getInstrumentation().getContext().getPackageManager() - .getPackageInfo("com.microsoft.aad.adal.testapp", PackageManager.GET_SIGNATURES); + final Context context = getInstrumentation().getContext(); + PackageInfo info = context.getPackageManager() + .getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES); for (Signature signature : info.signatures) { mTestSignature = signature.toByteArray(); MessageDigest md = MessageDigest.getInstance("SHA"); diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationActivityUnitTest.java b/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationActivityUnitTest.java index aa1ad6fd5..4325f753f 100644 --- a/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationActivityUnitTest.java +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationActivityUnitTest.java @@ -169,40 +169,6 @@ public void testReturnToCaller() throws IllegalArgumentException, NoSuchFieldExc data.getIntExtra(AuthenticationConstants.Browser.REQUEST_ID, 0)); } - @SmallTest - @UiThreadTest - public void testWebviewInstallLink() throws IllegalArgumentException, - NoSuchFieldException, IllegalAccessException, InvocationTargetException, - ClassNotFoundException, NoSuchMethodException, InstantiationException, - InterruptedException, ExecutionException { - startActivity(mIntentToStartActivity, null, null); - mActivity = getActivity(); - String url = AuthenticationConstants.Broker.BROWSER_EXT_INSTALL_PREFIX - + "?username=abc@outlook.com&app_link=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.azure.authenticator"; - WebViewClient client = getCustomWebViewClient(); - WebView mockview = new WebView(getActivity().getApplicationContext()); - ReflectionUtils.setFieldValue(mActivity, "mSpinner", null); - - // Act - client.shouldOverrideUrlLoading(mockview, url); - - // Verify result code that includes requestid. Activity will set the - // result back to caller. - TestLogResponse response = new TestLogResponse(); - final CountDownLatch signal = new CountDownLatch(1); - response.listenForLogMessage("It is an install request", signal); - int counter = 0; - final int maxWaitCycles = 20; - while (!isFinishCalled() && counter < maxWaitCycles) { - Thread.sleep(DEVICE_RESPONSE_WAIT); - counter++; - } - - String savedData = ApplicationReceiver.getInstallRequestInthisApp(getInstrumentation().getTargetContext()); - assertNotNull(savedData); - assertTrue(savedData.contains("abc@outlook.com")); - } - /** * Return authentication exception at setResult so that mActivity receives at * onActivityResult diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationContextTest.java b/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationContextTest.java index 0a9083fae..da05d9453 100644 --- a/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationContextTest.java +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationContextTest.java @@ -122,7 +122,7 @@ protected void setUp() throws Exception { } AuthenticationSettings.INSTANCE.setUseBroker(false); // ADAL is set to this signature for now - PackageInfo info = mContext.getPackageManager().getPackageInfo(TEST_PACKAGE_NAME, + PackageInfo info = mContext.getPackageManager().getPackageInfo(getContext().getPackageName(), PackageManager.GET_SIGNATURES); // Broker App can be signed with multiple certificates. It will look @@ -605,7 +605,7 @@ public void run() { @SmallTest public void testAcquireTokenByRefreshTokenConnectionNotAvailable() throws InterruptedException { FileMockContext mockContext = new FileMockContext(getContext()); - mockContext.setConnectionAvaliable(false); + mockContext.setConnectionAvailable(false); final AuthenticationContext context = new AuthenticationContext(mockContext, VALID_AUTHORITY, false); diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationParamsTests.java b/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationParamsTests.java index 4c93f057b..add4259b4 100644 --- a/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationParamsTests.java +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationParamsTests.java @@ -24,6 +24,7 @@ package com.microsoft.aad.adal; import java.io.IOException; +import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.HttpURLConnection; @@ -56,9 +57,18 @@ public void testGetResource() { assertTrue("resource should be null", param.getResource() == null); } - public void testCreateFromResourceUrlInvalidFormat() { + public void testCreateFromResourceUrlInvalidFormat() throws IOException { Log.d(TAG, "test:" + getName() + "thread:" + android.os.Process.myTid()); + //mock http response + final HttpURLConnection mockedConnection = Mockito.mock(HttpURLConnection.class); + HttpUrlConnectionFactory.setMockedHttpUrlConnection(mockedConnection); + Util.prepareMockedUrlConnection(mockedConnection); + Mockito.when(mockedConnection.getOutputStream()).thenReturn(Mockito.mock(OutputStream.class)); + Mockito.when(mockedConnection.getInputStream()).thenReturn( + Util.createInputStream(Util.getSuccessTokenResponse(false, false))); + Mockito.when(mockedConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + final TestResponse testResponse = new TestResponse(); setupAsyncParamRequest("http://www.cnn.com", testResponse); diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationRequestTests.java b/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationRequestTests.java index b2a2a6c2a..13ff0cdd2 100644 --- a/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationRequestTests.java +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/AuthenticationRequestTests.java @@ -26,8 +26,6 @@ import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.util.UUID; public class AuthenticationRequestTests extends AndroidTestCase { @@ -45,93 +43,53 @@ protected void tearDown() throws Exception { } @SmallTest - public void testAuthenticationRequestParams() throws NoSuchMethodException, - ClassNotFoundException, IllegalArgumentException, InstantiationException, - IllegalAccessException, InvocationTargetException { + public void testAuthenticationRequestParams() { + AuthenticationRequest request = new AuthenticationRequest(); + assertNull("authority is null", request.getAuthority()); - Constructor constructor = Class.forName( - ReflectionUtils.TEST_PACKAGE_NAME + ".AuthenticationRequest") - .getDeclaredConstructor(); - constructor.setAccessible(true); - Object o = constructor.newInstance(); - - String actual = ReflectionUtils.getterValue(String.class, o, "getAuthority"); - assertNull("authority is null", actual); // call with params - o = ReflectionUtils.getInstance(ReflectionUtils.TEST_PACKAGE_NAME - + ".AuthenticationRequest", "authority1", "resource2", "client3", false); - actual = ReflectionUtils.getterValue(String.class, o, "getAuthority"); - assertEquals("authority is same", "authority1", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getResource"); - assertEquals("resource is same", "resource2", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getClientId"); - assertEquals("client is same", "client3", actual); - - o = ReflectionUtils.getInstance(ReflectionUtils.TEST_PACKAGE_NAME - + ".AuthenticationRequest", "authority31", "resource32", "client33", "redirect34", - "loginhint35", false); - - actual = ReflectionUtils.getterValue(String.class, o, "getAuthority"); - assertEquals("authority is same", "authority31", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getResource"); - assertEquals("resource is same", "resource32", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getClientId"); - assertEquals("client is same", "client33", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getRedirectUri"); - assertEquals("redirect is same", "redirect34", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getLoginHint"); - assertEquals("loginhint is same", "loginhint35", actual); + request = new AuthenticationRequest("authority1", "resource2", "client3", false); + assertEquals("authority is same", "authority1", request.getAuthority()); + assertEquals("resource is same", "resource2", request.getResource()); + assertEquals("client is same", "client3", request.getClientId()); + + request = new AuthenticationRequest("authority31", "resource32", "client33", "redirect34", "loginhint35", false); + assertEquals("authority is same", "authority31", request.getAuthority()); + assertEquals("resource is same", "resource32", request.getResource()); + assertEquals("client is same", "client33", request.getClientId()); + assertEquals("redirect is same", "redirect34", request.getRedirectUri()); + assertEquals("loginhint is same", "loginhint35", request.getLoginHint()); UUID correlationId = UUID.randomUUID(); - o = ReflectionUtils.getInstance(ReflectionUtils.TEST_PACKAGE_NAME - + ".AuthenticationRequest", "authority41", "resource42", "client43", "redirect44", - "loginhint45", correlationId, false); - - actual = ReflectionUtils.getterValue(String.class, o, "getAuthority"); - assertEquals("authority is same", "authority41", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getResource"); - assertEquals("resource is same", "resource42", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getClientId"); - assertEquals("client is same", "client43", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getRedirectUri"); - assertEquals("redirect is same", "redirect44", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getLoginHint"); - assertEquals("loginhint is same", "loginhint45", actual); - UUID actualId = ReflectionUtils.getterValue(UUID.class, o, "getCorrelationId"); - assertEquals("correlationId is same", correlationId, actualId); - - o = ReflectionUtils.getInstance(ReflectionUtils.TEST_PACKAGE_NAME - + ".AuthenticationRequest", "authority51", "resource52", "client53", "redirect54", - "loginhint55", PromptBehavior.Always, "extraQueryPAram56", correlationId, false); - - actual = ReflectionUtils.getterValue(String.class, o, "getAuthority"); - assertEquals("authority is same", "authority51", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getResource"); - assertEquals("resource is same", "resource52", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getClientId"); - assertEquals("client is same", "client53", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getRedirectUri"); - assertEquals("redirect is same", "redirect54", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getLoginHint"); - assertEquals("loginhint is same", "loginhint55", actual); - actual = ReflectionUtils.getterValue(String.class, o, "getExtraQueryParamsAuthentication"); - assertEquals("ExtraQueryParams is same", "extraQueryPAram56", actual); - PromptBehavior actualPrompt = ReflectionUtils.getterValue(PromptBehavior.class, o, "getPrompt"); - assertEquals("PromptBehavior is same", PromptBehavior.Always, actualPrompt); - actualId = ReflectionUtils.getterValue(UUID.class, o, "getCorrelationId"); - assertEquals("correlationId is same", correlationId, actualId); + request = new AuthenticationRequest("authority41", "resource42", "client43", "redirect44", "loginhint45", correlationId, false); + assertEquals("authority is same", "authority41", request.getAuthority()); + assertEquals("resource is same", "resource42", request.getResource()); + assertEquals("client is same", "client43", request.getClientId()); + assertEquals("redirect is same", "redirect44", request.getRedirectUri()); + assertEquals("loginhint is same", "loginhint45", request.getLoginHint()); + assertEquals("correlationId is same", correlationId, request.getCorrelationId()); + + + request = new AuthenticationRequest("authority51", "resource52", "client53", "redirect54", + "loginhint55", PromptBehavior.Always, "extraQueryPAram56", correlationId, false, "testClaims"); + assertEquals("authority is same", "authority51", request.getAuthority()); + assertEquals("resource is same", "resource52", request.getResource()); + assertEquals("client is same", "client53", request.getClientId()); + assertEquals("redirect is same", "redirect54", request.getRedirectUri()); + assertEquals("loginhint is same", "loginhint55", request.getLoginHint()); + assertEquals("ExtraQueryParams is same", "extraQueryPAram56", request.getExtraQueryParamsAuthentication()); + assertEquals("PromptBehavior is same", PromptBehavior.Always, request.getPrompt()); + assertEquals("correlationId is same", correlationId, request.getCorrelationId()); + assertEquals("claimsChallenge is same", "testClaims", request.getClaimsChallenge()); } @SmallTest - public void testRequestId() throws IllegalArgumentException, ClassNotFoundException, - NoSuchMethodException, InstantiationException, IllegalAccessException, - InvocationTargetException { - Object o = ReflectionUtils.getInstance(ReflectionUtils.TEST_PACKAGE_NAME - + ".AuthenticationRequest", "authority1", "resource2", "client3", false); - ReflectionUtils.setterValue(o, "setRequestId", Integer.valueOf(REQUEST_ID)); - int actual = ReflectionUtils.getterValue(Integer.class, o, "getRequestId"); - assertEquals("Same RequestId", REQUEST_ID, actual); + public void testRequestId() { + final AuthenticationRequest request = new AuthenticationRequest("authority1", "resource2", "client3", false); + request.setRequestId(REQUEST_ID); + + assertEquals("Same RequestId", REQUEST_ID, request.getRequestId()); } @SmallTest diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/BrokerAccountServiceTest.java b/adal/src/androidTest/java/com/microsoft/aad/adal/BrokerAccountServiceTest.java index f029254fa..62f5b5ba5 100644 --- a/adal/src/androidTest/java/com/microsoft/aad/adal/BrokerAccountServiceTest.java +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/BrokerAccountServiceTest.java @@ -49,10 +49,14 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Collections; +import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import static com.microsoft.aad.adal.OauthTests.createAuthenticationRequest; +import static org.mockito.Mockito.mock; + /** * Test cases for brokerAccountService and related operations in {@link BrokerProxy}. */ @@ -166,7 +170,7 @@ public void testGetAuthTokenVerifyNoNetwork() throws InterruptedException, Authe public void run() { final Context mockContext = getMockContext(); Bundle requestBundle = new Bundle(); - requestBundle.putString("isConnectionAvaliable","false"); + requestBundle.putString("isConnectionAvailable","false"); try { final Bundle bundle = BrokerAccountServiceHandler.getInstance().getAuthToken(mockContext, requestBundle); @@ -206,6 +210,29 @@ public void run() { latch.await(); } + public void testGetIntentContainsSkipCacheAndClaimsForBrokerActivity() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + + sThreadExecutor.execute(new Runnable() { + @Override + public void run() { + final String claimsChallenge = "testClaims"; + final AuthenticationRequest authRequest = createAuthenticationRequest("https://login.windows.net/omercantest", "resource", "client", + "redirect", "loginhint", PromptBehavior.Auto, "", UUID.randomUUID(), false); + authRequest.setClaimsChallenge(claimsChallenge); + + final Context context = getMockContext(); + final BrokerProxy brokerProxy = new BrokerProxy(context); + + final Intent intent = brokerProxy.getIntentForBrokerActivity(authRequest); + assertTrue(Boolean.toString(true).equals(intent.getStringExtra(AuthenticationConstants.Broker.BROKER_SKIP_CACHE))); + assertTrue(claimsChallenge.equals(intent.getStringExtra(AuthenticationConstants.Broker.ACCOUNT_CLAIMS))); + latch.countDown(); + } + }); + latch.await(); + } + /** * Verify even if GET_ACCOUNTS permission is not granted, if BrokerAccountService exists, * {@link BrokerProxy#canSwitchToBroker(String)} will return true. diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/BrokerProxyTests.java b/adal/src/androidTest/java/com/microsoft/aad/adal/BrokerProxyTests.java index caf3b1227..80f849323 100644 --- a/adal/src/androidTest/java/com/microsoft/aad/adal/BrokerProxyTests.java +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/BrokerProxyTests.java @@ -93,7 +93,7 @@ protected void setUp() throws Exception { System.setProperty("dexmaker.dexcache", getContext().getCacheDir().getPath()); // ADAL is set to this signature for now - PackageInfo info = mContext.getPackageManager().getPackageInfo("com.microsoft.aad.adal.testapp", + PackageInfo info = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), PackageManager.GET_SIGNATURES); // Broker App can be signed with multiple certificates. It will look @@ -706,7 +706,7 @@ public void testGetAuthTokenInBackgroundVerifyErrorNoNetworkConnection() throws final AuthenticationRequest authRequest = createAuthenticationRequest("https://login.windows.net/omercantest", "resource", "client", "redirect", acctName.toLowerCase(Locale.US), PromptBehavior.Auto, "", UUID.randomUUID(), false); final FileMockContext context = new FileMockContext(getContext()); - context.setConnectionAvaliable(false); + context.setConnectionAvailable(false); final AccountManager mockedAccountManager = getMockedAccountManager(AuthenticationConstants.Broker.BROKER_ACCOUNT_TYPE, AuthenticationConstants.Broker.COMPANY_PORTAL_APP_PACKAGE_NAME); setMockProxyForErrorCheck(mockedAccountManager, acctName, AccountManager.ERROR_CODE_NETWORK_ERROR, ADALError.DEVICE_CONNECTION_IS_NOT_AVAILABLE.getDescription()); @@ -975,7 +975,7 @@ private static AuthenticationRequest createAuthenticationRequest(final String au final String extraQueryParams, final UUID correlationId, boolean isExtendedLifetimeEnabled) { return new AuthenticationRequest(authority, resource, client, redirect, loginHint, prompt, extraQueryParams, - correlationId, isExtendedLifetimeEnabled); + correlationId, isExtendedLifetimeEnabled, null); } private PackageManager getMockedPackageManagerWithBrokerAccountServiceDisabled(final Signature signature, diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/FileMockContext.java b/adal/src/androidTest/java/com/microsoft/aad/adal/FileMockContext.java index 912d8bffc..7db86c957 100644 --- a/adal/src/androidTest/java/com/microsoft/aad/adal/FileMockContext.java +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/FileMockContext.java @@ -64,7 +64,7 @@ class FileMockContext extends MockContext { private Map mPermissionMap = new HashMap(); - private boolean mIsConnectionAvaliable = true; + private boolean mIsConnectionAvailable = true; private AccountManager mMockedAccountManager = null; @@ -109,7 +109,7 @@ public Object getSystemService(String name) { } else if (name.equalsIgnoreCase("connectivity")) { final ConnectivityManager mockedConnectivityManager = mock(ConnectivityManager.class); final NetworkInfo mockedNetworkInfo = mock(NetworkInfo.class); - Mockito.when(mockedNetworkInfo.isConnectedOrConnecting()).thenReturn(mIsConnectionAvaliable); + Mockito.when(mockedNetworkInfo.isConnectedOrConnecting()).thenReturn(mIsConnectionAvailable); Mockito.when(mockedConnectivityManager.getActiveNetworkInfo()).thenReturn(mockedNetworkInfo); return mockedConnectivityManager; } @@ -170,8 +170,8 @@ public int checkPermission(String permName, String pkgName) { return PackageManager.PERMISSION_DENIED; } - public void setConnectionAvaliable(boolean connectionAvaliable) { - mIsConnectionAvaliable = connectionAvaliable; + public void setConnectionAvailable(boolean connectionAvailable) { + mIsConnectionAvailable = connectionAvailable; } public void setResolveIntent(boolean resolveIntent) { diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/HttpDialogTests.java b/adal/src/androidTest/java/com/microsoft/aad/adal/HttpDialogTests.java index 8239bd4fb..99b83ecbf 100644 --- a/adal/src/androidTest/java/com/microsoft/aad/adal/HttpDialogTests.java +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/HttpDialogTests.java @@ -23,8 +23,6 @@ package com.microsoft.aad.adal; -import java.security.MessageDigest; - import android.annotation.SuppressLint; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -33,6 +31,8 @@ import android.util.Base64; import android.util.Log; +import java.security.MessageDigest; + public class HttpDialogTests extends AndroidTestCase { private static final String TAG = "HttpDialogTests"; @@ -49,7 +49,7 @@ protected void setUp() throws Exception { System.setProperty("dexmaker.dexcache", getContext().getCacheDir().getPath()); // ADAL is set to this signature for now - PackageInfo info = mContext.getPackageManager().getPackageInfo("com.microsoft.aad.adal.testapp", + PackageInfo info = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), PackageManager.GET_SIGNATURES); // Broker App can be signed with multiple certificates. It will look diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/MockBrokerAccountService.java b/adal/src/androidTest/java/com/microsoft/aad/adal/MockBrokerAccountService.java index edb743ca3..e8c349911 100644 --- a/adal/src/androidTest/java/com/microsoft/aad/adal/MockBrokerAccountService.java +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/MockBrokerAccountService.java @@ -30,8 +30,6 @@ import android.os.IBinder; import android.os.RemoteException; -import org.mockito.Mockito; - import java.util.Map; /** @@ -76,7 +74,7 @@ public synchronized Bundle getBrokerUsers() throws RemoteException { @Override public synchronized Bundle acquireTokenSilently(Map requestParameters) throws RemoteException { final Bundle bundle = new Bundle(); - if(requestParameters.containsKey("isConnectionAvaliable")) { + if(requestParameters.containsKey("isConnectionAvailable")) { bundle.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_NETWORK_ERROR); bundle.putString(AccountManager.KEY_ERROR_MESSAGE, ADALError.DEVICE_CONNECTION_IS_NOT_AVAILABLE.getDescription()); } else { @@ -88,7 +86,7 @@ public synchronized Bundle acquireTokenSilently(Map requestParameters) throws Re @Override public Intent getIntentForInteractiveRequest() throws RemoteException { - return Mockito.mock(Intent.class); + return new Intent(); } @Override diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/OauthTests.java b/adal/src/androidTest/java/com/microsoft/aad/adal/OauthTests.java index 72899f52a..7ba1f3420 100644 --- a/adal/src/androidTest/java/com/microsoft/aad/adal/OauthTests.java +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/OauthTests.java @@ -255,6 +255,27 @@ public void testGetCodeRequestUrl() throws UnsupportedEncodingException { assertTrue("Prompt", actualCodeRequestQPHasChrome.contains("&prompt=login&extra=1&haschrome=1")); } + public void testGetCodeRequestUrlWithClaims() throws UnsupportedEncodingException { + final UUID correlationId = UUID.randomUUID(); + final AuthenticationRequest request = new AuthenticationRequest("authority51", "resource52", "client53", "redirect54", + "loginhint55", PromptBehavior.Always, "p=extraQueryPAram56", correlationId, false, "testClaims"); + + final Oauth2 oauth2 = createOAuthInstance(request); + final String codeRequestUrl = oauth2.getCodeRequestUrl(); + assertTrue(codeRequestUrl.contains("claims=testClaims")); + assertTrue(codeRequestUrl.contains("p=extraQueryPAram56")); + } + + public void testGetCodeRequestUrlWithClaimsInExtraQP() throws UnsupportedEncodingException { + final UUID correlationId = UUID.randomUUID(); + final AuthenticationRequest request = new AuthenticationRequest("authority51", "resource52", "client53", "redirect54", + "loginhint55", PromptBehavior.Always, "claims=testclaims&a=b", correlationId, false, null); + + final Oauth2 oauth2 = createOAuthInstance(request); + final String codeRequestUrl = oauth2.getCodeRequestUrl(); + assertTrue(codeRequestUrl.contains("claims=testclaims&a=b")); + } + @SmallTest public void testGetCodeRequestUrlClientTrace() throws UnsupportedEncodingException { // with login hint @@ -695,7 +716,7 @@ public static AuthenticationRequest createAuthenticationRequest(final String aut final boolean isExtendedLifetimeEnabled) { return new AuthenticationRequest(authority, resource, client, redirect, loginhint, prompt, - extraQueryParams, correlationId, isExtendedLifetimeEnabled); + extraQueryParams, correlationId, isExtendedLifetimeEnabled, null); } diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/PackageHelperTests.java b/adal/src/androidTest/java/com/microsoft/aad/adal/PackageHelperTests.java index 17d07d2f5..df416aca7 100644 --- a/adal/src/androidTest/java/com/microsoft/aad/adal/PackageHelperTests.java +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/PackageHelperTests.java @@ -23,8 +23,15 @@ package com.microsoft.aad.adal; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.Signature; +import android.test.AndroidTestCase; +import android.util.Base64; import java.io.UnsupportedEncodingException; import java.lang.reflect.Constructor; @@ -38,19 +45,11 @@ import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.Signature; -import android.test.AndroidTestCase; -import android.util.Base64; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class PackageHelperTests extends AndroidTestCase { - private static final String TEST_PACKAGE_NAME = "com.microsoft.aad.adal.testapp"; private static final int TEST_UID = 13; private byte[] mTestSignature; @@ -76,7 +75,7 @@ protected void setUp() throws Exception { AuthenticationSettings.INSTANCE.setBrokerPackageName("invalid_do_not_switch"); AuthenticationSettings.INSTANCE.setBrokerSignature("invalid_do_not_switch"); // ADAL is set to this signature for now - PackageInfo info = mContext.getPackageManager().getPackageInfo(TEST_PACKAGE_NAME, + PackageInfo info = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), PackageManager.GET_SIGNATURES); // Broker App can be signed with multiple certificates. It will look @@ -99,13 +98,13 @@ protected void tearDown() throws Exception { public void testGetCurrentSignatureForPackage() throws NameNotFoundException, IllegalArgumentException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { - Context mockContext = getMockContext(new Signature(mTestSignature), TEST_PACKAGE_NAME, 0); + Context mockContext = getMockContext(new Signature(mTestSignature), mContext.getPackageName(), 0); Object packageHelper = getInstance(mockContext); Method m = ReflectionUtils.getTestMethod(packageHelper, "getCurrentSignatureForPackage", String.class); // act - String actual = (String) m.invoke(packageHelper, TEST_PACKAGE_NAME); + String actual = (String) m.invoke(packageHelper, mContext.getPackageName()); // assert assertEquals("should be same info", mTestTag, actual); @@ -120,13 +119,13 @@ public void testGetCurrentSignatureForPackage() throws NameNotFoundException, public void testGetUIDForPackage() throws NameNotFoundException, IllegalArgumentException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { - Context mockContext = getMockContext(new Signature(mTestSignature), TEST_PACKAGE_NAME, + Context mockContext = getMockContext(new Signature(mTestSignature), mContext.getPackageName(), TEST_UID); Object packageHelper = getInstance(mockContext); Method m = ReflectionUtils.getTestMethod(packageHelper, "getUIDForPackage", String.class); // act - int actual = (Integer) m.invoke(packageHelper, TEST_PACKAGE_NAME); + int actual = (Integer) m.invoke(packageHelper, mContext.getPackageName()); // assert assertEquals("should be same UID", TEST_UID, actual); @@ -141,16 +140,16 @@ public void testGetUIDForPackage() throws NameNotFoundException, IllegalArgument public void testRedirectUrl() throws NameNotFoundException, IllegalArgumentException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, UnsupportedEncodingException { - Context mockContext = getMockContext(new Signature(mTestSignature), TEST_PACKAGE_NAME, 0); + Context mockContext = getMockContext(new Signature(mTestSignature), mContext.getPackageName(), 0); Object packageHelper = getInstance(mockContext); Method m = ReflectionUtils.getTestMethod(packageHelper, "getBrokerRedirectUrl", String.class, String.class); // act - String actual = (String) m.invoke(packageHelper, TEST_PACKAGE_NAME, mTestTag); + String actual = (String) m.invoke(packageHelper, mContext.getPackageName(), mTestTag); // assert - assertTrue("should have packagename", actual.contains(TEST_PACKAGE_NAME)); + assertTrue("should have packagename", actual.contains(mContext.getPackageName())); assertTrue("should have signature url encoded", actual.contains(URLEncoder.encode(mTestTag, AuthenticationConstants.ENCODING_UTF8))); } diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/StorageHelperTests.java b/adal/src/androidTest/java/com/microsoft/aad/adal/StorageHelperTests.java index e2effd194..033255196 100644 --- a/adal/src/androidTest/java/com/microsoft/aad/adal/StorageHelperTests.java +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/StorageHelperTests.java @@ -26,6 +26,7 @@ import android.annotation.TargetApi; import android.content.Context; import android.os.Build; +import android.support.test.filters.Suppress; import android.util.Base64; import android.util.Log; @@ -249,6 +250,9 @@ public void testVersion() throws GeneralSecurityException, IOException { } } + + //Github issue #580. Suppress this unit test as we cannot make it work consistently. + @Suppress @TargetApi(MIN_SDK_VERSION) public void testKeyPair() throws GeneralSecurityException, IOException { diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/UtilityTest.java b/adal/src/androidTest/java/com/microsoft/aad/adal/UtilityTest.java new file mode 100644 index 000000000..3e2de6763 --- /dev/null +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/UtilityTest.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.microsoft.aad.adal; + +import junit.framework.Assert; + +import java.util.UUID; + +public class UtilityTest extends AndroidTestHelper { + + public void testClaimsPassedInWithParameter() { + final AuthenticationRequest request = new AuthenticationRequest("authority51", "resource52", "client53", "redirect54", + "loginhint55", PromptBehavior.Always, "extraQueryPAram56", UUID.randomUUID(), false, "testClaims"); + + Assert.assertTrue(Utility.isClaimsChallengePresent(request)); + } +} diff --git a/adal/src/androidTest/java/com/microsoft/aad/adal/WebRequestHandlerTests.java b/adal/src/androidTest/java/com/microsoft/aad/adal/WebRequestHandlerTests.java index ace46b15e..8388d37fc 100644 --- a/adal/src/androidTest/java/com/microsoft/aad/adal/WebRequestHandlerTests.java +++ b/adal/src/androidTest/java/com/microsoft/aad/adal/WebRequestHandlerTests.java @@ -27,6 +27,7 @@ import android.util.Log; import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; import com.microsoft.aad.adal.AuthenticationConstants.AAD; import org.mockito.Mockito; @@ -55,7 +56,7 @@ public final class WebRequestHandlerTests extends AndroidTestHelper { /** * send invalid request to production service * - * @throws IOException + * @throws IOException */ @SmallTest public void testCorrelationIdInRequest() throws IOException { @@ -231,12 +232,19 @@ public void testPostRequest() throws IOException { } class TestMessage { - @com.google.gson.annotations.SerializedName("AccessToken") + @SerializedName("AccessToken") private String mAccessToken; - @com.google.gson.annotations.SerializedName("UserName") + @SerializedName("UserName") private String mUserName; + /** + * No args constructor for use in serialization for Gson to prevent usage of sun.misc.Unsafe + */ + @SuppressWarnings("unused") + private TestMessage() { + } + public TestMessage(String token, String name) { mAccessToken = token; mUserName = name; diff --git a/adal/src/main/java/com/microsoft/aad/adal/AcquireTokenInteractiveRequest.java b/adal/src/main/java/com/microsoft/aad/adal/AcquireTokenInteractiveRequest.java index 76ea9e907..3f6f915f8 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/AcquireTokenInteractiveRequest.java +++ b/adal/src/main/java/com/microsoft/aad/adal/AcquireTokenInteractiveRequest.java @@ -55,7 +55,7 @@ final class AcquireTokenInteractiveRequest { void acquireToken(final IWindowComponent activity, final AuthenticationDialog dialog) throws AuthenticationException { //Check if there is network connection - HttpWebRequest.throwIfNetworkNotAvaliable(mContext); + HttpWebRequest.throwIfNetworkNotAvailable(mContext); // Update the PromptBehavior. Since we add the new prompt behavior(force_prompt) for broker apps to // force prompt, if this flag is set in the embeded flow, we need to update it to always. For embed diff --git a/adal/src/main/java/com/microsoft/aad/adal/AcquireTokenRequest.java b/adal/src/main/java/com/microsoft/aad/adal/AcquireTokenRequest.java index 20d0066c8..189469276 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/AcquireTokenRequest.java +++ b/adal/src/main/java/com/microsoft/aad/adal/AcquireTokenRequest.java @@ -58,7 +58,6 @@ class AcquireTokenRequest { private final IBrokerProxy mBrokerProxy; private Handler mHandler = null; - private BrokerResumeResultReceiver mBrokerResumeResultReceiver = null; /** * Instance validation related calls are serviced inside Discovery as a @@ -320,7 +319,9 @@ private AuthenticationResult tryAcquireTokenSilent(final AuthenticationRequest a private boolean shouldTrySilentFlow(final AuthenticationRequest authenticationRequest) { - return authenticationRequest.getPrompt() == PromptBehavior.Auto || authenticationRequest.isSilent(); + return !Utility.isClaimsChallengePresent(authenticationRequest) + && authenticationRequest.getPrompt() == PromptBehavior.Auto + || authenticationRequest.isSilent(); } /** @@ -435,7 +436,7 @@ private void acquireTokenInteractiveFlow(final CallbackHandler callbackHandle, + " Cannot launch webview, acitivity is null."); } - HttpWebRequest.throwIfNetworkNotAvaliable(mContext); + HttpWebRequest.throwIfNetworkNotAvailable(mContext); final int requestId = callbackHandle.getCallback().hashCode(); authenticationRequest.setRequestId(requestId); @@ -737,7 +738,7 @@ private void waitingRequestOnError(final CallbackHandler handler, final Authenti } } } finally { - if (exc != null && exc.getCode() != ADALError.AUTH_FAILED_CANCELLED) { + if (exc != null) { mAuthContext.removeWaitingRequest(requestId); } } @@ -787,82 +788,4 @@ AuthenticationCallback getCallback() { return mCallback; } } - - /** - * Responsible for receiving message from broker indicating the broker has completed the token acquisition. - */ - protected class BrokerResumeResultReceiver extends BroadcastReceiver { - public BrokerResumeResultReceiver() { } - - private boolean mReceivedResultFromBroker = false; - - @Override - public void onReceive(Context context, Intent intent) { - final String methodName = ":BrokerResumeResultReceiver:onReceive"; - Logger.d(TAG + methodName, "Received result from broker."); - final int receivedWaitingRequestId = intent.getIntExtra(AuthenticationConstants.Browser.REQUEST_ID, 0); - - if (receivedWaitingRequestId == 0) { - Logger.v(TAG + methodName, "Received waiting request is 0, error will be thrown, cannot find correct " - + "callback to send back the result."); - // Cannot throw AuthenticationException which no longer - // extending from RuntimeException. Will log the error - // and return back to caller. - return; - } - - // Setting flag to show that receiver already receive result from broker - mReceivedResultFromBroker = true; - final AuthenticationRequestState waitingRequest; - try { - waitingRequest = mAuthContext.getWaitingRequest(receivedWaitingRequestId); - } catch (final AuthenticationException authenticationException) { - Logger.e(TAG, "No waiting request exists", "", ADALError.CALLBACK_IS_NOT_FOUND, - authenticationException); - (new ContextWrapper(mContext)).unregisterReceiver(mBrokerResumeResultReceiver); - return; - } - - final String errorCode = intent.getStringExtra(AuthenticationConstants.Browser.RESPONSE_ERROR_CODE); - if (!StringExtensions.isNullOrBlank(errorCode)) { - final String errorMessage = intent.getStringExtra( - AuthenticationConstants.Browser.RESPONSE_ERROR_MESSAGE); - final String returnedErrorMessage = "ErrorCode: " + errorCode + " ErrorMessage" + errorMessage - + mAuthContext.getCorrelationInfoFromWaitingRequest(waitingRequest); - Logger.v(TAG + methodName, returnedErrorMessage); - waitingRequestOnError(waitingRequest, receivedWaitingRequestId, - new AuthenticationException(ADALError.AUTH_FAILED, returnedErrorMessage)); - } else { - final boolean isBrokerCompleteTokenRequest = intent.getBooleanExtra( - AuthenticationConstants.Broker.BROKER_RESULT_RETURNED, false); - if (isBrokerCompleteTokenRequest) { - Logger.v(TAG + methodName, "Broker already completed the token request, calling " - + "acquireTokenSilentSync to retrieve token from broker."); - final AuthenticationRequest authenticationRequest = waitingRequest.getRequest(); - String userId = intent.getStringExtra(AuthenticationConstants.Broker.ACCOUNT_USERINFO_USERID); - - // For acquireTokenSilentSync, uniqueId should be passed. - if (StringExtensions.isNullOrBlank(userId)) { - userId = authenticationRequest.getUserId(); - } - - authenticationRequest.setSilent(true); - authenticationRequest.setUserId(userId); - authenticationRequest.setUserIdentifierType(AuthenticationRequest.UserIdentifierType.UniqueId); - acquireToken(null, false, authenticationRequest, waitingRequest.getDelegate()); - } else { - Logger.v(TAG + methodName, "Broker doesn't send back error nor the completion notification."); - waitingRequestOnError(waitingRequest, receivedWaitingRequestId, - new AuthenticationException(ADALError.AUTH_FAILED, - "Broker doesn't send back error nor the completion notification.")); - } - } - (new ContextWrapper(mContext)).unregisterReceiver(mBrokerResumeResultReceiver); - } - - public boolean isResultReceivedFromBroker() { - return mReceivedResultFromBroker; - } - } - } diff --git a/adal/src/main/java/com/microsoft/aad/adal/AcquireTokenSilentHandler.java b/adal/src/main/java/com/microsoft/aad/adal/AcquireTokenSilentHandler.java index 078588f8a..fc15e0683 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/AcquireTokenSilentHandler.java +++ b/adal/src/main/java/com/microsoft/aad/adal/AcquireTokenSilentHandler.java @@ -117,7 +117,7 @@ AuthenticationResult acquireTokenWithRefreshToken(final String refreshToken) mAuthRequest.getLogInfo(), null); // Check if network is available, if not throw exception. - HttpWebRequest.throwIfNetworkNotAvaliable(mContext); + HttpWebRequest.throwIfNetworkNotAvailable(mContext); final AuthenticationResult result; try { @@ -189,6 +189,11 @@ private AuthenticationResult tryRT() throws AuthenticationException { Logger.v(TAG, statusMessage); return tryMRRT(); } + + if (StringExtensions.isNullOrBlank(mAuthRequest.getUserFromRequest()) && mTokenCacheAccessor.isMultipleRTsMatchingGivenAppAndResource( + mAuthRequest.getClientId(), mAuthRequest.getResource())) { + throw new AuthenticationException(ADALError.AUTH_FAILED_USER_MISMATCH, "Multiple refresh tokens exists for the given client id and resource"); + } Logger.v(TAG, "Send request to use regular RT for new AT."); return acquireTokenWithCachedItem(regularRTItem); @@ -227,6 +232,10 @@ private AuthenticationResult tryMRRT() throws AuthenticationException { // Pass the failed MRRT result to tryFRT, if FRT does not exist, return the MRRT result. mrrtResult = tryFRT(familyClientId, mrrtResult); } + + if (StringExtensions.isNullOrBlank(mAuthRequest.getUserFromRequest()) && mTokenCacheAccessor.isMultipleMRRTsMatchingGivenApp(mAuthRequest.getClientId())) { + throw new AuthenticationException(ADALError.AUTH_FAILED_USER_MISMATCH, "No User provided and multiple MRRTs exist for the given client id"); + } return mrrtResult; } diff --git a/adal/src/main/java/com/microsoft/aad/adal/ApplicationReceiver.java b/adal/src/main/java/com/microsoft/aad/adal/ApplicationReceiver.java deleted file mode 100644 index 1cd97f4db..000000000 --- a/adal/src/main/java/com/microsoft/aad/adal/ApplicationReceiver.java +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package com.microsoft.aad.adal; - -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.List; -import java.util.TimeZone; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import com.google.gson.Gson; - -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; - -/** - * Receives system broadcast message for application install events. You need to - * register this receiver in your manifest for PACKAGE_INSTALL and - * PACKAGE_ADDED. - */ -public class ApplicationReceiver extends BroadcastReceiver { - - private static final String TAG = ApplicationReceiver.class.getSimpleName() + ":"; - - /** - * Shared preference to track broker install. - */ - public static final String INSTALL_REQUEST_TRACK_FILE = "adal.broker.install.track"; - - /** - * Shared preference key for install request. - */ - public static final String INSTALL_REQUEST_KEY = "adal.broker.install.request"; - - /** - * Shared preference timestamp for install. - */ - public static final String INSTALL_REQUEST_TIMESTAMP_KEY = "adal.broker.install.request.timestamp"; - - private static final String INSTALL_UPN_KEY = "username"; - - /** - * Application link to open in the browser. - */ - public static final String INSTALL_URL_KEY = "app_link"; - - // Allow 5 mins for broker app to be installed - private static final int BROKER_APP_INSTALLATION_TIME_OUT = 5; - - private BrokerProxy mBrokerProxy; - - /** - * This method receives message for any application status based on filters - * defined in your manifest. - * - * @param context ApplicationContext - * @param intent to get the installed package name - */ - @Override - public void onReceive(Context context, Intent intent) { - // Check if the application is install and belongs to the broker package - final String methodName = "onReceive"; - if (!intent.getAction().equals(Intent.ACTION_PACKAGE_ADDED) || intent.getData() == null) { - return; - } - Logger.v(TAG + methodName, "Application install message is received"); - Logger.v(TAG + methodName, "ApplicationReceiver detectes the installation of " + intent.getData().toString()); - final String receivedInstalledPackageName = intent.getData().toString(); - if (receivedInstalledPackageName.equalsIgnoreCase("package:" - + AuthenticationConstants.Broker.AZURE_AUTHENTICATOR_APP_PACKAGE_NAME) - || receivedInstalledPackageName.equalsIgnoreCase("package:" - + AuthenticationSettings.INSTANCE.getBrokerPackageName())) { - - String request = getInstallRequestInthisApp(context); - mBrokerProxy = new BrokerProxy(context); - final Date dateTimeForSavedRequest = new Date(getInstallRequestTimeStamp(context)); - - // Broker request will be resumed if - // 1) there is saved request in sharedPreference - // 2) app has the correct configuration to get token from broker - // 3) the saved request is not timeout - if (!StringExtensions.isNullOrBlank(request) && mBrokerProxy.canSwitchToBroker("") == BrokerProxy.SwitchToBroker.CAN_SWITCH_TO_BROKER - && isRequestTimestampValidForResume(dateTimeForSavedRequest)) { - Logger.v(TAG + methodName, receivedInstalledPackageName + " is installed, start sending request to broker."); - resumeRequestInBroker(context, request); - } else { - Logger.v(TAG + methodName, "No request saved in sharedpreferences or request already timeout" - + ", cannot resume broker request."); - } - } - } - - /** - * Save request fields into shared preference. - * - * @param ctx application context - * @param request AuthenticationRequest object - * @param url request url - */ - public static void saveRequest(final Context ctx, final AuthenticationRequest request, final String url) { - final String methodName = "saveRequest"; - - Logger.v(TAG + methodName, "ApplicationReceiver starts to save the request in shared preference."); - SharedPreferences prefs = ctx.getSharedPreferences(INSTALL_REQUEST_TRACK_FILE, - Activity.MODE_PRIVATE); - - if (prefs != null) { - HashMap parameters = StringExtensions.getUrlParameters(url); - if (parameters != null && parameters.containsKey(INSTALL_UPN_KEY)) { - Logger.v(TAG + methodName, "Coming redirect contains the UPN, setting it on the request for both loginhint and broker account name."); - request.setLoginHint(parameters.get(INSTALL_UPN_KEY)); - request.setBrokerAccountName(parameters.get(INSTALL_UPN_KEY)); - } - Editor prefsEditor = prefs.edit(); - Gson gson = new Gson(); - String jsonRequest = gson.toJson(request); - prefsEditor.putString(INSTALL_REQUEST_KEY, jsonRequest); - - // Also saving the timestamp - final Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); - prefsEditor.putLong(INSTALL_REQUEST_TIMESTAMP_KEY, calendar.getTimeInMillis()); - - prefsEditor.apply(); - } else { - Logger.v(TAG + methodName, "SharedPreference is null, nothing saved."); - } - } - - /** - * Get username that started the install flow. - * - * @param ctx app/activity context - * @return the username that started the install flow - */ - public static String getUserName(Context ctx) { - Logger.v(TAG, "ApplicationReceiver:getUserName"); - String request = getInstallRequestInthisApp(ctx); - if (!StringExtensions.isNullOrBlank(request)) { - Gson gson = new Gson(); - AuthenticationRequest pendingRequest = gson.fromJson(request, - AuthenticationRequest.class); - if (pendingRequest != null) { - return pendingRequest.getBrokerAccountName(); - } - } - - return null; - } - - /** - * Read install request key from shared preference. - * - * @param context application context - * @return the saved request stored in SharedPreference - */ - public static String getInstallRequestInthisApp(final Context context) { - final String methodName = "getInstallRequestInthisApp"; - - Logger.v(TAG + methodName, "Retrieve saved request from shared preference."); - SharedPreferences prefs = context.getSharedPreferences(INSTALL_REQUEST_TRACK_FILE, - Activity.MODE_PRIVATE); - if (prefs != null && prefs.contains(INSTALL_REQUEST_KEY)) { - String request = prefs.getString(INSTALL_REQUEST_KEY, ""); - Logger.d(TAG + methodName, "Install request:" + request); - return request; - } - - Logger.v(TAG + methodName, "Unable to retrieve saved request from shared preference."); - return ""; - } - - /** - * Clear the username after resuming login. - * - * @param ctx app/activity context - */ - public static void clearUserName(Context ctx) { - Logger.v(TAG, "ApplicationReceiver:clearUserName"); - SharedPreferences prefs = ctx.getSharedPreferences(INSTALL_REQUEST_TRACK_FILE, - Activity.MODE_PRIVATE); - if (prefs != null) { - Editor prefsEditor = prefs.edit(); - prefsEditor.putString(INSTALL_REQUEST_KEY, ""); - prefsEditor.apply(); - } - } - - private boolean isRequestTimestampValidForResume(final Date savedRequestTimestamp) { - final String methodName = "isRequestTimestampValidForResume"; - - // Get current UTC time - final Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); - calendar.add(Calendar.MINUTE, BROKER_APP_INSTALLATION_TIME_OUT * (-1)); - if (savedRequestTimestamp.compareTo(calendar.getTime()) >= 0) { - Logger.v(TAG + methodName, "Saved request is valid, not timeout yet."); - return true; - } - - Logger.v(TAG + methodName, "Saved request is already timeout"); - return false; - } - - private void resumeRequestInBroker(final Context context, final String request) { - final String methodName = "resumeRequestInBroker"; - Logger.v(TAG + methodName, "Start resuming request in broker"); - Gson gson = new Gson(); - final AuthenticationRequest pendingRequest = gson.fromJson(request, AuthenticationRequest.class); - ExecutorService sThreadExecutor = Executors.newSingleThreadExecutor(); - - sThreadExecutor.execute(new Runnable() { - @Override - public void run() { - Logger.v(TAG + methodName, "Running task in thread:" + android.os.Process.myTid() + ", trying to get intent for " - + "broker activity."); - final Intent resumeIntent = mBrokerProxy.getIntentForBrokerActivity(pendingRequest); - resumeIntent.setAction(Intent.ACTION_PICK); - - Logger.v(TAG + methodName, "Setting flag for broker resume request for calling package " + context.getPackageName()); - resumeIntent.putExtra(AuthenticationConstants.Broker.BROKER_REQUEST_RESUME, - AuthenticationConstants.Broker.BROKER_REQUEST_RESUME); - resumeIntent.putExtra(AuthenticationConstants.Broker.CALLER_INFO_PACKAGE, context.getPackageName()); - - final String brokerProtocolVersion = resumeIntent.getStringExtra(AuthenticationConstants.Broker.BROKER_VERSION); - if (StringExtensions.isNullOrBlank(brokerProtocolVersion)) { - Logger.v(TAG + methodName, "Broker request resume is not supported in the older version of broker."); - return; - } - - PackageManager packageManager = context.getPackageManager(); - // Get activities that can handle the intent - List activities = packageManager.queryIntentActivities(resumeIntent, 0); - - // Check if 1 or more were returned - boolean isIntentSafe = activities.size() > 0; - - if (isIntentSafe) { - Logger.v(TAG + methodName, "It's safe to start .ui.AccountChooserActivity."); - resumeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); - context.startActivity(resumeIntent); - } else { - Logger.v(TAG + methodName, "Unable to resolve .ui.AccountChooserActivity."); - } - } - }); - } - - private long getInstallRequestTimeStamp(final Context context) { - final String methodName = "getInstallRequestTimeStamp"; - - Logger.v(TAG + methodName, "Retrieve timestamp for saved request from shared preference."); - SharedPreferences prefs = context.getSharedPreferences(INSTALL_REQUEST_TRACK_FILE, - Activity.MODE_PRIVATE); - if (prefs != null && prefs.contains(INSTALL_REQUEST_TIMESTAMP_KEY)) { - final long savedRequestTimeStamp = prefs.getLong(INSTALL_REQUEST_TIMESTAMP_KEY, 0); - Logger.v(TAG + methodName, "Timestamp for saved request is: " + savedRequestTimeStamp); - return savedRequestTimeStamp; - } - - return 0; - } -} diff --git a/adal/src/main/java/com/microsoft/aad/adal/AuthenticationActivity.java b/adal/src/main/java/com/microsoft/aad/adal/AuthenticationActivity.java index 3bbefbd97..6b647a1c6 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/AuthenticationActivity.java +++ b/adal/src/main/java/com/microsoft/aad/adal/AuthenticationActivity.java @@ -65,6 +65,7 @@ import android.webkit.ClientCertRequest; import android.webkit.CookieManager; import android.webkit.CookieSyncManager; +import android.webkit.WebSettings; import android.webkit.WebView; /** @@ -368,6 +369,10 @@ public boolean onTouch(View view, MotionEvent event) { mWebView.getSettings().setDomStorageEnabled(true); mWebView.getSettings().setUseWideViewPort(true); mWebView.getSettings().setBuiltInZoomControls(true); + + // WebSettings.LOAD_CACHE_ELSE_NETWORK makes the webview go to the server if the cached resource has + // expired. This should prevent err_cach_miss errors when hitting back from an page marked no_cache + mWebView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); mWebView.setWebViewClient(new CustomWebViewClient()); mWebView.setVisibility(View.INVISIBLE); } diff --git a/adal/src/main/java/com/microsoft/aad/adal/AuthenticationConstants.java b/adal/src/main/java/com/microsoft/aad/adal/AuthenticationConstants.java index 27b04d020..a18ec5813 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/AuthenticationConstants.java +++ b/adal/src/main/java/com/microsoft/aad/adal/AuthenticationConstants.java @@ -215,6 +215,8 @@ public static final class OAuth2 { static final String HAS_CHROME = "haschrome"; static final String EXT_EXPIRES_IN = "ext_expires_in"; + + static final String CLAIMS = "claims"; } /** @@ -315,6 +317,8 @@ public static final class Broker { /** String of broker protocl version with PRT support. */ public static final String BROKER_PROTOCOL_VERSION = "v2"; + public static final String BROKER_SKIP_CACHE = "skip.cache"; + /** String of broker result returned. */ public static final String BROKER_RESULT_RETURNED = "broker.result.returned"; @@ -339,6 +343,9 @@ public static final class Broker { /** String of account extra query param. */ public static final String ACCOUNT_EXTRA_QUERY_PARAM = "account.extra.query.param"; + /** String of account claims. */ + public static final String ACCOUNT_CLAIMS = "account.claims"; + /** String of account login hint. */ public static final String ACCOUNT_LOGIN_HINT = "account.login.hint"; diff --git a/adal/src/main/java/com/microsoft/aad/adal/AuthenticationContext.java b/adal/src/main/java/com/microsoft/aad/adal/AuthenticationContext.java index 1f935c006..405fd5a9a 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/AuthenticationContext.java +++ b/adal/src/main/java/com/microsoft/aad/adal/AuthenticationContext.java @@ -267,7 +267,7 @@ && checkADFSValidationRequirements(loginHint, callback)) { final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri, loginHint, PromptBehavior.Auto, null, - getRequestCorrelationId(), getExtendedLifetimeEnabled()); + getRequestCorrelationId(), getExtendedLifetimeEnabled(), null); request.setUserIdentifierType(UserIdentifierType.LoginHint); request.setTelemetryRequestId(requestId); createAcquireTokenRequest(apiEvent).acquireToken(wrapActivity(activity), false, request, callback); @@ -308,7 +308,7 @@ && checkADFSValidationRequirements(loginHint, callback)) { final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri, loginHint, PromptBehavior.Auto, extraQueryParameters, - getRequestCorrelationId(), getExtendedLifetimeEnabled()); + getRequestCorrelationId(), getExtendedLifetimeEnabled(), null); request.setUserIdentifierType(UserIdentifierType.LoginHint); request.setTelemetryRequestId(requestId); createAcquireTokenRequest(apiEvent).acquireToken(wrapActivity(activity), false, request, callback); @@ -345,7 +345,7 @@ && checkADFSValidationRequirements(null, callback)) { apiEvent.setPromptBehavior(prompt.toString()); final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, - clientId, redirectUri, null, prompt, null, getRequestCorrelationId(), getExtendedLifetimeEnabled()); + clientId, redirectUri, null, prompt, null, getRequestCorrelationId(), getExtendedLifetimeEnabled(), null); request.setTelemetryRequestId(requestId); @@ -384,7 +384,7 @@ && checkADFSValidationRequirements(null, callback)) { final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri, null, prompt, extraQueryParameters, - getRequestCorrelationId(), getExtendedLifetimeEnabled()); + getRequestCorrelationId(), getExtendedLifetimeEnabled(), null); request.setTelemetryRequestId(requestId); @@ -426,7 +426,51 @@ && checkADFSValidationRequirements(loginHint, callback)) { final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri, loginHint, prompt, extraQueryParameters, - getRequestCorrelationId(), getExtendedLifetimeEnabled()); + getRequestCorrelationId(), getExtendedLifetimeEnabled(), null); + request.setUserIdentifierType(UserIdentifierType.LoginHint); + request.setTelemetryRequestId(requestId); + createAcquireTokenRequest(apiEvent).acquireToken(wrapActivity(activity), false, request, callback); + } + } + + /** + * acquireToken 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 if validateAuthority == null. 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 claims Optional. The claims challenge returned from middle tier service, will be added as query string + * to authorize endpoint. + * @param callback required {@link AuthenticationCallback} object for async + * call. + */ + public void acquireToken(final Activity activity, final String resource, final String clientId, @Nullable String redirectUri, + @Nullable final String loginHint, @Nullable final PromptBehavior prompt, @Nullable String extraQueryParameters, + @Nullable final String claims, final AuthenticationCallback callback) { + throwIfClaimsInBothExtraQpAndClaimsParameter(claims, extraQueryParameters); + + if (checkPreRequirements(resource, clientId, callback) + && checkADFSValidationRequirements(loginHint, callback)) { + redirectUri = getRedirectUri(redirectUri); + final String requestId = Telemetry.registerNewRequest(); + final APIEvent apiEvent = createApiEvent(mContext, clientId, requestId, EventStrings.ACQUIRE_TOKEN_8); + apiEvent.setPromptBehavior(prompt.toString()); + apiEvent.setLoginHint(loginHint); + + final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, + clientId, redirectUri, loginHint, prompt, extraQueryParameters, + getRequestCorrelationId(), getExtendedLifetimeEnabled(), claims); request.setUserIdentifierType(UserIdentifierType.LoginHint); request.setTelemetryRequestId(requestId); createAcquireTokenRequest(apiEvent).acquireToken(wrapActivity(activity), false, request, callback); @@ -466,7 +510,50 @@ && checkADFSValidationRequirements(loginHint, callback)) { final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri, loginHint, prompt, extraQueryParameters, - getRequestCorrelationId(), getExtendedLifetimeEnabled()); + getRequestCorrelationId(), getExtendedLifetimeEnabled(), null); + request.setUserIdentifierType(UserIdentifierType.LoginHint); + request.setTelemetryRequestId(requestId); + createAcquireTokenRequest(apiEvent).acquireToken(fragment, 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 if validateAuthority == null. 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 claims Optional. The claims challenge returned from middle tier service, will be added as query string + * to authorize endpoint. + * @param callback required {@link AuthenticationCallback} object for async + * call. + */ + public void acquireToken(final IWindowComponent fragment, final String resource, final String clientId, @Nullable String redirectUri, + @Nullable final String loginHint, @Nullable final PromptBehavior prompt, @Nullable String extraQueryParameters, + @Nullable final String claims, final AuthenticationCallback callback) { + throwIfClaimsInBothExtraQpAndClaimsParameter(claims, extraQueryParameters); + + if (checkPreRequirements(resource, clientId, callback) + && checkADFSValidationRequirements(loginHint, callback)) { + redirectUri = getRedirectUri(redirectUri); + final String requestId = Telemetry.registerNewRequest(); + final APIEvent apiEvent = createApiEvent(mContext, clientId, requestId, EventStrings.ACQUIRE_TOKEN_9); + apiEvent.setPromptBehavior(prompt.toString()); + apiEvent.setLoginHint(loginHint); + + final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, + clientId, redirectUri, loginHint, prompt, extraQueryParameters, + getRequestCorrelationId(), getExtendedLifetimeEnabled(), claims); request.setUserIdentifierType(UserIdentifierType.LoginHint); request.setTelemetryRequestId(requestId); createAcquireTokenRequest(apiEvent).acquireToken(fragment, false, request, callback); @@ -507,7 +594,7 @@ && checkADFSValidationRequirements(loginHint, callback)) { final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, clientId, redirectUri, loginHint, prompt, extraQueryParameters, - getRequestCorrelationId(), getExtendedLifetimeEnabled()); + getRequestCorrelationId(), getExtendedLifetimeEnabled(), null); request.setUserIdentifierType(UserIdentifierType.LoginHint); request.setTelemetryRequestId(requestId); @@ -515,6 +602,50 @@ && checkADFSValidationRequirements(loginHint, 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 if validateAuthority == null. 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 claims Optional. The claims challenge returned from middle tier service, will be added as query string + * to authorize endpoint. + * @param callback required {@link AuthenticationCallback} object for async + * call. + */ + public void acquireToken(final String resource, final String clientId, @Nullable String redirectUri, + @Nullable final String loginHint, @Nullable final PromptBehavior prompt, @Nullable String extraQueryParameters, + @Nullable final String claims, final AuthenticationCallback callback) { + throwIfClaimsInBothExtraQpAndClaimsParameter(claims, extraQueryParameters); + + if (checkPreRequirements(resource, clientId, callback) + && checkADFSValidationRequirements(loginHint, callback)) { + redirectUri = getRedirectUri(redirectUri); + final String requestId = Telemetry.registerNewRequest(); + final APIEvent apiEvent = createApiEvent(mContext, clientId, requestId, EventStrings.ACQUIRE_TOKEN_10); + apiEvent.setPromptBehavior(prompt.toString()); + apiEvent.setLoginHint(loginHint); + + final AuthenticationRequest request = new AuthenticationRequest(mAuthority, resource, + clientId, redirectUri, loginHint, prompt, extraQueryParameters, + getRequestCorrelationId(), getExtendedLifetimeEnabled(), claims); + request.setUserIdentifierType(UserIdentifierType.LoginHint); + request.setTelemetryRequestId(requestId); + createAcquireTokenRequest(apiEvent).acquireToken(null, false, request, callback); + } + } + /** * This is sync function. It will first look at the cache and automatically * checks for the token expiration. Additionally, if no suitable access @@ -563,21 +694,12 @@ public AuthenticationResult acquireTokenSilentSync(String resource, String clien new AuthenticationCallback() { @Override public void onSuccess(AuthenticationResult result) { - apiEvent.setWasApiCallSuccessful(true, null); - apiEvent.setCorrelationId(request.getCorrelationId().toString()); - apiEvent.setIdToken(result.getIdToken()); - apiEvent.stopTelemetryAndFlush(); - authenticationResult.set(result); latch.countDown(); } @Override public void onError(Exception exc) { - apiEvent.setWasApiCallSuccessful(false, exc); - apiEvent.setCorrelationId(request.getCorrelationId().toString()); - apiEvent.stopTelemetryAndFlush(); - exception.set(exc); latch.countDown(); } @@ -872,9 +994,8 @@ public void onActivityResult(final int requestCode, final int resultCode, final * * @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 + * @return true: if there is a valid waiting request and cancel message sent + * successfully or if no waiting request exists. false: If the request could not be cancelled * @throws AuthenticationException if failed to get the waiting request */ public boolean cancelAuthenticationActivity(final int requestId) throws AuthenticationException { @@ -1213,7 +1334,7 @@ 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.12.0"; + return "1.13.0"; } /** @@ -1250,4 +1371,10 @@ private APIEvent createApiEvent(Context context, String clientId, String request Telemetry.getInstance().startEvent(requestId, apiEvent.getEventName()); return apiEvent; } + + private void throwIfClaimsInBothExtraQpAndClaimsParameter(final String claims, final String extraQP) { + if (!StringExtensions.isNullOrBlank(claims) && !StringExtensions.isNullOrBlank(extraQP) && extraQP.contains(AuthenticationConstants.OAuth2.CLAIMS)) { + throw new IllegalArgumentException("claims cannot be sent in claims parameter and extra qp."); + } + } } diff --git a/adal/src/main/java/com/microsoft/aad/adal/AuthenticationRequest.java b/adal/src/main/java/com/microsoft/aad/adal/AuthenticationRequest.java index 40aa6104c..a5a79f1bd 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/AuthenticationRequest.java +++ b/adal/src/main/java/com/microsoft/aad/adal/AuthenticationRequest.java @@ -71,6 +71,8 @@ class AuthenticationRequest implements Serializable { private String mTelemetryRequestId; + private String mClaimsChallenge; + /** * Developer can use acquireToken(with loginhint) or acquireTokenSilent(with * userid), so this sets the type of the request. @@ -84,7 +86,8 @@ public AuthenticationRequest() { } public AuthenticationRequest(String authority, String resource, String client, String redirect, - String loginhint, PromptBehavior prompt, String extraQueryParams, UUID correlationId, boolean isExtendedLifetimeEnabled) { + String loginhint, PromptBehavior prompt, String extraQueryParams, UUID correlationId, + boolean isExtendedLifetimeEnabled, final String claimsChallenge) { mAuthority = authority; mResource = resource; mClientId = client; @@ -96,6 +99,7 @@ public AuthenticationRequest(String authority, String resource, String client, S mCorrelationId = correlationId; mIdentifierType = UserIdentifierType.NoUser; mIsExtendedLifetimeEnabled = isExtendedLifetimeEnabled; + mClaimsChallenge = claimsChallenge; } public AuthenticationRequest(String authority, String resource, String client, String redirect, @@ -263,6 +267,14 @@ public boolean getIsExtendedLifetimeEnabled() { return mIsExtendedLifetimeEnabled; } + public void setClaimsChallenge(final String claimsChallenge) { + mClaimsChallenge = claimsChallenge; + } + + public String getClaimsChallenge() { + return mClaimsChallenge; + } + /** * Get either loginhint or user id based what's passed in the request. */ diff --git a/adal/src/main/java/com/microsoft/aad/adal/BasicWebViewClient.java b/adal/src/main/java/com/microsoft/aad/adal/BasicWebViewClient.java index 073ecd0c9..722929433 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/BasicWebViewClient.java +++ b/adal/src/main/java/com/microsoft/aad/adal/BasicWebViewClient.java @@ -42,6 +42,10 @@ abstract class BasicWebViewClient extends WebViewClient { + /** + * Application link to open in the browser. + */ + private static final String INSTALL_URL_KEY = "app_link"; private static final String TAG = "BasicWebViewClient"; public static final String BLANK_PAGE = "about:blank"; @@ -263,7 +267,6 @@ public void run() { return true; } else if (url.startsWith(AuthenticationConstants.Broker.BROWSER_EXT_INSTALL_PREFIX)) { Logger.v(TAG, "It is an install request"); - ApplicationReceiver.saveRequest(mCallingContext, mRequest, url); HashMap parameters = StringExtensions .getUrlParameters(url); prepareForBrokerResumeRequest(); @@ -278,7 +281,7 @@ public void run() { } catch (InterruptedException e) { Logger.v(TAG + ":shouldOverrideUrlLoading", "Error occured when having thread sleeping for 1 second"); } - openLinkInBrowser(parameters.get(ApplicationReceiver.INSTALL_URL_KEY)); + openLinkInBrowser(parameters.get(INSTALL_URL_KEY)); view.stopLoading(); return true; } diff --git a/adal/src/main/java/com/microsoft/aad/adal/BrokerAccountServiceHandler.java b/adal/src/main/java/com/microsoft/aad/adal/BrokerAccountServiceHandler.java index dd5412582..4b76651a9 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/BrokerAccountServiceHandler.java +++ b/adal/src/main/java/com/microsoft/aad/adal/BrokerAccountServiceHandler.java @@ -245,7 +245,7 @@ private Map prepareGetAuthTokenRequestData(final Context context final Map requestData = new HashMap<>(); for (final String key : requestBundleKeys) { - if (key == AuthenticationConstants.Browser.REQUEST_ID) { + if (key.equals(AuthenticationConstants.Browser.REQUEST_ID)) { requestData.put(key, String.valueOf(requestBundle.getInt(key))); continue; } diff --git a/adal/src/main/java/com/microsoft/aad/adal/BrokerProxy.java b/adal/src/main/java/com/microsoft/aad/adal/BrokerProxy.java index 516d804b5..dc2bf323e 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/BrokerProxy.java +++ b/adal/src/main/java/com/microsoft/aad/adal/BrokerProxy.java @@ -653,6 +653,12 @@ private Bundle getBrokerOptions(final AuthenticationRequest request) { if (request.getPrompt() != null) { brokerOptions.putString(AuthenticationConstants.Broker.ACCOUNT_PROMPT, request.getPrompt().name()); } + + if (Utility.isClaimsChallengePresent(request)) { + brokerOptions.putString(AuthenticationConstants.Broker.BROKER_SKIP_CACHE, Boolean.toString(true)); + brokerOptions.putString(AuthenticationConstants.Broker.ACCOUNT_CLAIMS, request.getClaimsChallenge()); + } + return brokerOptions; } diff --git a/adal/src/main/java/com/microsoft/aad/adal/DRSMetadata.java b/adal/src/main/java/com/microsoft/aad/adal/DRSMetadata.java index 0d2bdbd38..eaf2462bf 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/DRSMetadata.java +++ b/adal/src/main/java/com/microsoft/aad/adal/DRSMetadata.java @@ -33,6 +33,13 @@ final class DRSMetadata { @SerializedName("IdentityProviderService") private IdentityProviderService mIdentityProviderService; + /** + * No args constructor for use in serialization for Gson to prevent usage of sun.misc.Unsafe + */ + @SuppressWarnings("unused") + DRSMetadata() { + } + /** * Gets the IdentityProviderService. * diff --git a/adal/src/main/java/com/microsoft/aad/adal/Discovery.java b/adal/src/main/java/com/microsoft/aad/adal/Discovery.java index 9334071bc..98fd926c8 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/Discovery.java +++ b/adal/src/main/java/com/microsoft/aad/adal/Discovery.java @@ -186,6 +186,7 @@ private void initValidList() { VALID_HOSTS.add("login.chinacloudapi.cn"); // Microsoft Azure China VALID_HOSTS.add("login.microsoftonline.de"); // Microsoft Azure Germany VALID_HOSTS.add("login-us.microsoftonline.com"); // Microsoft Azure US Government + VALID_HOSTS.add("login.microsoftonline.us"); // Microsoft Azure US } } diff --git a/adal/src/main/java/com/microsoft/aad/adal/HttpWebRequest.java b/adal/src/main/java/com/microsoft/aad/adal/HttpWebRequest.java index 7a145b295..9fe46bf13 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/HttpWebRequest.java +++ b/adal/src/main/java/com/microsoft/aad/adal/HttpWebRequest.java @@ -168,7 +168,7 @@ public HttpWebResponse send() throws IOException { return response; } - static void throwIfNetworkNotAvaliable(final Context context) throws AuthenticationException { + static void throwIfNetworkNotAvailable(final Context context) throws AuthenticationException { final DefaultConnectionService connectionService = new DefaultConnectionService(context); if (!connectionService.isConnectionAvailable()) { AuthenticationException authenticationException = new AuthenticationException( diff --git a/adal/src/main/java/com/microsoft/aad/adal/IdentityProviderService.java b/adal/src/main/java/com/microsoft/aad/adal/IdentityProviderService.java index 38f95379d..b2529c5a4 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/IdentityProviderService.java +++ b/adal/src/main/java/com/microsoft/aad/adal/IdentityProviderService.java @@ -35,6 +35,13 @@ final class IdentityProviderService { @SerializedName("PassiveAuthEndpoint") private String mPassiveAuthEndpoint; + /** + * No args constructor for use in serialization for Gson to prevent usage of sun.misc.Unsafe + */ + @SuppressWarnings("unused") + IdentityProviderService() { + } + /** * Gets the PassiveAuthEndpoint. * diff --git a/adal/src/main/java/com/microsoft/aad/adal/JWSBuilder.java b/adal/src/main/java/com/microsoft/aad/adal/JWSBuilder.java index c1a867c67..8929bb689 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/JWSBuilder.java +++ b/adal/src/main/java/com/microsoft/aad/adal/JWSBuilder.java @@ -23,6 +23,7 @@ package com.microsoft.aad.adal; +import com.google.gson.annotations.SerializedName; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -59,28 +60,42 @@ class JWSBuilder implements IJWSBuilder { * Payload for JWS. */ class Claims { - @com.google.gson.annotations.SerializedName("aud") + @SerializedName("aud") private String mAudience; - @com.google.gson.annotations.SerializedName("iat") + @SerializedName("iat") private long mIssueAt; - @com.google.gson.annotations.SerializedName("nonce") + @SerializedName("nonce") private String mNonce; + + /** + * No args constructor for use in serialization for Gson to prevent usage of sun.misc.Unsafe + */ + @SuppressWarnings("unused") + private Claims() { + } } /** * Header that includes algorithm, type, thumbprint, keys, and keyid. */ class JwsHeader { - @com.google.gson.annotations.SerializedName("alg") + @SerializedName("alg") private String mAlgorithm; - @com.google.gson.annotations.SerializedName("typ") + @SerializedName("typ") private String mType; - @com.google.gson.annotations.SerializedName("x5c") + @SerializedName("x5c") private String[] mCert; + + /** + * No args constructor for use in serialization for Gson to prevent usage of sun.misc.Unsafe + */ + @SuppressWarnings("unused") + private JwsHeader() { + } } /** @@ -160,7 +175,7 @@ public String generateSignedJWT(String nonce, String audience, RSAPrivateKey pri /** * Signs the input with the private key. - * + * * @param privateKey the key to sign input with * @param input the data that needs to be signed * @return String signed string diff --git a/adal/src/main/java/com/microsoft/aad/adal/Link.java b/adal/src/main/java/com/microsoft/aad/adal/Link.java index cfbf882e4..207f24f3e 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/Link.java +++ b/adal/src/main/java/com/microsoft/aad/adal/Link.java @@ -44,6 +44,13 @@ final class Link { @SerializedName("href") private String mHref; + /** + * No args constructor for use in serialization for Gson to prevent usage of sun.misc.Unsafe + */ + @SuppressWarnings("unused") + Link() { + } + /** * Gets the rel. * diff --git a/adal/src/main/java/com/microsoft/aad/adal/Oauth2.java b/adal/src/main/java/com/microsoft/aad/adal/Oauth2.java index 17cd8ec6b..ca1546590 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/Oauth2.java +++ b/adal/src/main/java/com/microsoft/aad/adal/Oauth2.java @@ -64,6 +64,8 @@ class Oauth2 { private static final int DELAY_TIME_PERIOD = 1000; + private static final int MAX_RESILIENCY_ERROR_CODE = 599; + private static final String DEFAULT_AUTHORIZE_ENDPOINT = "/oauth2/authorize"; private static final String DEFAULT_TOKEN_ENDPOINT = "/oauth2/token"; @@ -156,6 +158,12 @@ public String getAuthorizationEndpointQueryParameters() throws UnsupportedEncodi queryParameter.appendQueryParameter(AuthenticationConstants.OAuth2.HAS_CHROME, "1"); } + // Claims challenge are opaque to the sdk, we're not going to do any merging if both extra qp and claims parameter + // contain it. Also, if developer sends it in both places, server will fail it. + if (!StringExtensions.isNullOrBlank(mRequest.getClaimsChallenge())) { + queryParameter.appendQueryParameter(AuthenticationConstants.OAuth2.CLAIMS, mRequest.getClaimsChallenge()); + } + String requestUrl = queryParameter.build().getQuery(); if (!StringExtensions.isNullOrBlank(extraQP)) { String parsedQP = extraQP; @@ -629,25 +637,22 @@ private AuthenticationResult processTokenResponse(HttpWebResponse webResponse, f } final int statusCode = webResponse.getStatusCode(); - switch (statusCode) { - case HttpURLConnection.HTTP_OK: - case HttpURLConnection.HTTP_BAD_REQUEST: - case HttpURLConnection.HTTP_UNAUTHORIZED: - try { - result = parseJsonResponse(webResponse.getBody()); - if (result != null) { - httpEvent.setOauthErrorCode(result.getErrorCode()); - } - } catch (final JSONException jsonException) { - throw new AuthenticationException(ADALError.SERVER_INVALID_JSON_RESPONSE, "Can't parse server response " + webResponse.getBody(), jsonException); + + if (statusCode == HttpURLConnection.HTTP_OK + || statusCode == HttpURLConnection.HTTP_BAD_REQUEST + || statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + try { + result = parseJsonResponse(webResponse.getBody()); + if (result != null) { + httpEvent.setOauthErrorCode(result.getErrorCode()); } - break; - case HttpURLConnection.HTTP_INTERNAL_ERROR: - case HttpURLConnection.HTTP_GATEWAY_TIMEOUT: - case HttpURLConnection.HTTP_UNAVAILABLE: - throw new ServerRespondingWithRetryableException("Unexpected server response " + webResponse.getBody()); - default: - throw new AuthenticationException(ADALError.SERVER_ERROR, "Unexpected server response " + webResponse.getBody()); + } catch (final JSONException jsonException) { + throw new AuthenticationException(ADALError.SERVER_INVALID_JSON_RESPONSE, "Can't parse server response " + webResponse.getBody(), jsonException); + } + } else if (statusCode >= HttpURLConnection.HTTP_INTERNAL_ERROR && statusCode <= MAX_RESILIENCY_ERROR_CODE) { + throw new ServerRespondingWithRetryableException("Server Error " + statusCode + " " + webResponse.getBody()); + } else { + throw new AuthenticationException(ADALError.SERVER_ERROR, "Unexpected server response " + statusCode + " " + webResponse.getBody()); } // Set correlationId in the result diff --git a/adal/src/main/java/com/microsoft/aad/adal/SSOStateSerializer.java b/adal/src/main/java/com/microsoft/aad/adal/SSOStateSerializer.java index 74361e390..dd9669993 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/SSOStateSerializer.java +++ b/adal/src/main/java/com/microsoft/aad/adal/SSOStateSerializer.java @@ -62,15 +62,18 @@ final class SSOStateSerializer { .registerTypeAdapter(TokenCacheItem.class, new TokenCacheItemSerializationAdapater()) .create(); - private int getVersion() { - return version; + /** + * No args constructor for use in serialization for Gson to prevent usage of sun.misc.Unsafe + */ + @SuppressWarnings("unused") + private SSOStateSerializer() { } /** * constructor with an input item in type TokenCacheItem. We take * TokenCacheItem as input and call the constructor to initialize a * SSOStateSerializer object. - * + * * @param item TokenCacheItem to initialize a SSOStateSerializer */ private SSOStateSerializer(final TokenCacheItem item) { @@ -80,15 +83,13 @@ private SSOStateSerializer(final TokenCacheItem item) { this.mTokenCacheItems.add(item); } - /** - * Default constructor. - */ - private SSOStateSerializer() { + private int getVersion() { + return version; } /** * Return the token cache item in this blob container object. - * + * * @return tokenCacheItem * @throws AuthenticationException */ @@ -103,7 +104,7 @@ private TokenCacheItem getTokenItem() throws AuthenticationException { /** * serialize the tokenCacheItem with Adapter. - * + * * @return String */ private String internalSerialize() { @@ -112,9 +113,9 @@ private String internalSerialize() { /** * Deserialize the serializedBlob. - * + * * this function covers the details of the deserialization process - * + * * @param serializedBlob String blob to convert to TokenCacheItem * @return TokenCacheItem * @throws AuthenticationException @@ -140,7 +141,7 @@ private TokenCacheItem internalDeserialize(String serializedBlob) throws Authent * SSOStateSerializer on the serialization, we have this static serialize * function which takes the TokenCacheItem object as input and return the * serialized json string if successful. - * + * * @param item TokenCacheItem to convert to serialized json * @return String */ @@ -154,7 +155,7 @@ static String serialize(final TokenCacheItem item) { * SSOStateSerializer on the deserialization, we have this static * deserialize function take the serialized string as input and return the * deserialized TokenCacheItem if successful. - * + * * @param serializedBlob string blob to deserialize into TokenCacheItem * @return TokenCacheItem * @throws AuthenticationException diff --git a/adal/src/main/java/com/microsoft/aad/adal/TokenCacheAccessor.java b/adal/src/main/java/com/microsoft/aad/adal/TokenCacheAccessor.java index d0bb2d1a9..c94877aa8 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/TokenCacheAccessor.java +++ b/adal/src/main/java/com/microsoft/aad/adal/TokenCacheAccessor.java @@ -27,6 +27,7 @@ import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; /** @@ -70,6 +71,8 @@ TokenCacheItem getATFromCache(final String resource, final String clientId, fina Logger.v(TAG, "No access token exists."); return null; } + + throwIfMultipleATExisted(clientId, resource, user); if (!StringExtensions.isNullOrBlank(accessTokenItem.getAccessToken())) { if (TokenCacheItem.isTokenExpired(accessTokenItem.getExpiresOn())) { @@ -142,13 +145,14 @@ TokenCacheItem getFRTItem(final String familyClientId, final String user) { return item; } - TokenCacheItem getStaleToken(AuthenticationRequest authRequest) { + TokenCacheItem getStaleToken(AuthenticationRequest authRequest) throws AuthenticationException { final TokenCacheItem accessTokenItem = getRegularRefreshTokenCacheItem(authRequest.getResource(), authRequest.getClientId(), authRequest.getUserFromRequest()); if (accessTokenItem != null && !StringExtensions.isNullOrBlank(accessTokenItem.getAccessToken()) && accessTokenItem.getExtendedExpiresOn() != null && !TokenCacheItem.isTokenExpired(accessTokenItem.getExtendedExpiresOn())) { + throwIfMultipleATExisted(authRequest.getClientId(), authRequest.getResource(), authRequest.getUserFromRequest()); Logger.i(TAG, "The stale access token is returned.", ""); return accessTokenItem; } @@ -258,6 +262,34 @@ void removeTokenCacheItem(final TokenCacheItem tokenCacheItem, final String reso Telemetry.getInstance().stopEvent(mTelemetryRequestId, cacheEvent, EventStrings.TOKEN_CACHE_DELETE); } + + boolean isMultipleRTsMatchingGivenAppAndResource(final String clientId, final String resource) { + final Iterator allItems = mTokenCacheStore.getAll(); + final List regularRTsMatchingRequest = new ArrayList<>(); + while (allItems.hasNext()) { + final TokenCacheItem tokenCacheItem = allItems.next(); + if (tokenCacheItem.getAuthority().equalsIgnoreCase(mAuthority) && clientId.equalsIgnoreCase(tokenCacheItem.getClientId()) + && resource.equalsIgnoreCase(tokenCacheItem.getResource()) && !tokenCacheItem.getIsMultiResourceRefreshToken()) { + regularRTsMatchingRequest.add(tokenCacheItem); + } + } + + return regularRTsMatchingRequest.size() > 1; + } + + boolean isMultipleMRRTsMatchingGivenApp(final String clientId) { + final Iterator allItems = mTokenCacheStore.getAll(); + final List mrrtsMatchingRequest = new ArrayList<>(); + while (allItems.hasNext()) { + final TokenCacheItem tokenCacheItem = allItems.next(); + if (tokenCacheItem.getAuthority().equalsIgnoreCase(mAuthority) && tokenCacheItem.getClientId().equalsIgnoreCase(clientId) + && (tokenCacheItem.getIsMultiResourceRefreshToken() || StringExtensions.isNullOrBlank(tokenCacheItem.getResource()))) { + mrrtsMatchingRequest.add(tokenCacheItem); + } + } + + return mrrtsMatchingRequest.size() > 1; + } /** * Update token cache for a given user. If token is MRRT, store two separate entries for regular RT entry and MRRT entry. @@ -348,6 +380,13 @@ private boolean isUserMisMatch(final String user, final TokenCacheItem tokenCach return !user.equalsIgnoreCase(tokenCacheItem.getUserInfo().getDisplayableId()) && !user.equalsIgnoreCase(tokenCacheItem.getUserInfo().getUserId()); } + + private void throwIfMultipleATExisted(final String clientId, final String resource, final String user) throws AuthenticationException { + if (StringExtensions.isNullOrBlank(user) && isMultipleRTsMatchingGivenAppAndResource(clientId, resource)) { + throw new AuthenticationException(ADALError.AUTH_FAILED_USER_MISMATCH, "No user is provided and multiple access tokens " + + "exist for the given app and resource."); + } + } /** * Calculate hash for accessToken and log that. diff --git a/adal/src/main/java/com/microsoft/aad/adal/Utility.java b/adal/src/main/java/com/microsoft/aad/adal/Utility.java index 847be02b8..175201824 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/Utility.java +++ b/adal/src/main/java/com/microsoft/aad/adal/Utility.java @@ -41,5 +41,9 @@ static Date getImmutableDateObject(final Date date) { return date; } - + + static boolean isClaimsChallengePresent(final AuthenticationRequest request) { + // if developer pass claims down through extra qp, we should also skip cache. + return !StringExtensions.isNullOrBlank(request.getClaimsChallenge()); + } } diff --git a/adal/src/main/java/com/microsoft/aad/adal/WebFingerMetadata.java b/adal/src/main/java/com/microsoft/aad/adal/WebFingerMetadata.java index c9b441962..011fc3742 100644 --- a/adal/src/main/java/com/microsoft/aad/adal/WebFingerMetadata.java +++ b/adal/src/main/java/com/microsoft/aad/adal/WebFingerMetadata.java @@ -46,6 +46,13 @@ final class WebFingerMetadata { @SerializedName("links") private List mLinks; + /** + * No args constructor for use in serialization for Gson to prevent usage of sun.misc.Unsafe + */ + @SuppressWarnings("unused") + WebFingerMetadata() { + } + /** * Gets the subject. * diff --git a/adal/src/telemetry/java/com/microsoft/aad/adal/EventStrings.java b/adal/src/telemetry/java/com/microsoft/aad/adal/EventStrings.java index 6bcf92d6f..fd7cea79c 100644 --- a/adal/src/telemetry/java/com/microsoft/aad/adal/EventStrings.java +++ b/adal/src/telemetry/java/com/microsoft/aad/adal/EventStrings.java @@ -171,6 +171,12 @@ final class EventStrings { static final String ACQUIRE_TOKEN_7 = "117"; + static final String ACQUIRE_TOKEN_8 = "118"; + + static final String ACQUIRE_TOKEN_9 = "119"; + + static final String ACQUIRE_TOKEN_10 = "120"; + // Private constructor to prevent initialization private EventStrings() { // Intentionally left blank diff --git a/automationtestapp/.gitignore b/automationtestapp/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/automationtestapp/.gitignore @@ -0,0 +1 @@ +/build diff --git a/automationtestapp/build.gradle b/automationtestapp/build.gradle new file mode 100644 index 000000000..c3d5f4313 --- /dev/null +++ b/automationtestapp/build.gradle @@ -0,0 +1,53 @@ +/* + * // Copyright (c) Microsoft Corporation. + * // All rights reserved. + * // + * // This code is licensed under the MIT License. + * // + * // Permission is hereby granted, free of charge, to any person obtaining a copy + * // of this software and associated documentation files(the "Software"), to deal + * // in the Software without restriction, including without limitation the rights + * // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell + * // copies of the Software, and to permit persons to whom the Software is + * // furnished to do so, subject to the following conditions : + * // + * // The above copyright notice and this permission notice shall be included in + * // all copies or substantial portions of the Software. + * // + * // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * // THE SOFTWARE. + */ + +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.0" + + defaultConfig { + applicationId "com.microsoft.aad.automation.testapp" + minSdkVersion 14 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + testCompile 'junit:junit:4.12' + + compile 'com.android.support:appcompat-v7:25.0.0' + compile project(':adal') +} diff --git a/automationtestapp/proguard-rules.pro b/automationtestapp/proguard-rules.pro new file mode 100644 index 000000000..24c29cbbb --- /dev/null +++ b/automationtestapp/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\Program Files\Android\android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/automationtestapp/src/androidTest/java/com/microsoft/aad/automation/testapp/ApplicationTest.java b/automationtestapp/src/androidTest/java/com/microsoft/aad/automation/testapp/ApplicationTest.java new file mode 100644 index 000000000..6b6f795bd --- /dev/null +++ b/automationtestapp/src/androidTest/java/com/microsoft/aad/automation/testapp/ApplicationTest.java @@ -0,0 +1,13 @@ +package com.microsoft.aad.automation.testapp; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/automationtestapp/src/main/AndroidManifest.xml b/automationtestapp/src/main/AndroidManifest.xml new file mode 100644 index 000000000..f228e80ae --- /dev/null +++ b/automationtestapp/src/main/AndroidManifest.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/AndroidAutomationApp.java b/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/AndroidAutomationApp.java new file mode 100644 index 000000000..7d4173415 --- /dev/null +++ b/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/AndroidAutomationApp.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.microsoft.aad.automation.testapp; + +import android.app.Application; +import android.os.Build; +import android.webkit.WebView; + +import com.microsoft.aad.adal.ADALError; +import com.microsoft.aad.adal.AuthenticationSettings; +import com.microsoft.aad.adal.Logger; + +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * + */ +public class AndroidAutomationApp extends Application { + + private StringBuffer mADALLogs; + + @Override + public void onCreate() { + super.onCreate(); + mADALLogs = new StringBuffer(); + Logger.getInstance().setExternalLogger(new Logger.ILogger() { + + @Override + public void Log(String tag, String message, String additionalMessage, Logger.LogLevel level, ADALError errorCode) { + mADALLogs.append("tag:" + tag + ", message:" + message + ", additionalMessage:" + + additionalMessage + ", level:" + level + ", errorCode:" + errorCode + "\n"); + } + }); + setEncryptionKey(); + } + + private void setEncryptionKey() { + // Provide secret key for token encryption. + try { + // For API version lower than 18, you have to provide the secret key. The secret key + // needs to be 256 bits. You can use the following way to generate the secret key. And + // use AuthenticationSettings.Instance.setSecretKey(secretKeyBytes) to supply us the key. + // For API version 18 and above, we use android keystore to generate keypair, and persist + // the keypair in AnroidKeyStore. Current investigation shows 1)Keystore may be locked with + // a lock screen, if calling app has a lot of background activity, keystore cannot be + // accessed when locked, we'll be unable to decrypt the cache items 2) AndroidKeystore could + // be reset when gesture to unlock the device is changed. + // We do recommend the calling app the supply us the key with the above two limitations. + if (AuthenticationSettings.INSTANCE.getSecretKeyData() == null) { + // use same key for tests + SecretKeyFactory keyFactory = SecretKeyFactory + .getInstance("PBEWithSHA256And256BitAES-CBC-BC"); + SecretKey tempkey = keyFactory.generateSecret(new PBEKeySpec("test".toCharArray(), + "abcdedfdfd".getBytes("UTF-8"), 100, 256)); + SecretKey secretKey = new SecretKeySpec(tempkey.getEncoded(), "AES"); + AuthenticationSettings.INSTANCE.setSecretKey(secretKey.getEncoded()); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + WebView.setWebContentsDebuggingEnabled(true); + } + } catch (NoSuchAlgorithmException | InvalidKeySpecException | UnsupportedEncodingException ex) { + throw new IllegalStateException("Fail to generate secret key:" + ex.getMessage(), ex); + } + } + + public String getADALLogs() { + return mADALLogs.toString(); + } + + public void setADALLogs(String log) { + mADALLogs.append(log); + } +} diff --git a/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/Constants.java b/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/Constants.java new file mode 100644 index 000000000..54695a30e --- /dev/null +++ b/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/Constants.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.microsoft.aad.automation.testapp; + +/** + * Class holding the constants value. + */ +public class Constants { + public static final String ACCESS_TOKEN = "access_token"; + public static final String ACCESS_TOKEN_TYPE = "access_token_type"; + public static final String REFRESH_TOKEN = "refresh_token"; + public static final String EXPIRES_ON = "expires_on"; + public static final String TENANT_ID = "tenant_id"; + public static final String UNIQUE_ID = "unique_id"; + public static final String DISPLAYABLE_ID = "displayable_id"; + public static final String GIVEN_NAME = "given_name"; + public static final String FAMILY_NAME = "family_name"; + public static final String IDENTITY_PROVIDER = "identity_provider"; + public static final String ID_TOKEN = "id_token"; + + public static final String ERROR = "error"; + public static final String ERROR_DESCRIPTION = "error_description"; + public static final String ERROR_CAUSE = "error_cause"; + + public static final String READ_CACHE = "all_items"; + public static final String ITEM_COUNT = "item_count"; + public static final String EXPIRED_ACCESS_TOKEN_COUNT = "expired_access_token_count"; + public static final String INVALIDATED_REFRESH_TOKEN_COUNT = "invalidated_refresh_token_count"; + public static final String INVALIDATED_FAMILY_REFRESH_TOKEN_COUNT = "invalidated_family_refresh_token_count"; + public static final String CLEARED_TOKEN_COUNT = "cleared_token_count"; + public static final String READ_LOGS = "adal_logs"; + + public static final String JSON_ERROR = "json_error"; + + protected static class CACHE_DATA { + static final String ACCESS_TOKEN = Constants.ACCESS_TOKEN; + static final String REFRESH_TOKEN = Constants.REFRESH_TOKEN; + static final String RESOURCE = "resource"; + static final String AUTHORITY = "authority"; + static final String CLIENT_ID = "client_id"; + static final String RAW_ID_TOKEN = "id_token"; + static final String EXPIRES_ON = "expires_on"; + static final String IS_MRRT = "is_mrrt"; + static final String TENANT_ID = Constants.TENANT_ID; + static final String FAMILY_CLIENT_ID = "foci"; + static final String EXTENDED_EXPIRES_ON= "extended_expires_on"; + static final String UNIQUE_USER_ID = Constants.UNIQUE_ID; + static final String DISPLAYABLE_ID = Constants.DISPLAYABLE_ID; + static final String FAMILY_NAME = Constants.FAMILY_NAME; + static final String GIVEN_NAME = Constants.GIVEN_NAME; + static final String IDENTITY_PROVIDER = Constants.IDENTITY_PROVIDER; + } +} diff --git a/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/MainActivity.java b/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/MainActivity.java new file mode 100644 index 000000000..88b19aae9 --- /dev/null +++ b/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/MainActivity.java @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.microsoft.aad.automation.testapp; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.view.View; +import android.widget.Button; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.microsoft.aad.adal.ADALError; +import com.microsoft.aad.adal.AuthenticationContext; +import com.microsoft.aad.adal.DateTimeAdapter; +import com.microsoft.aad.adal.Logger; +import com.microsoft.aad.adal.TokenCacheItem; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; + +public class MainActivity extends AppCompatActivity { + + public static final String FLOW_CODE = "FlowCode"; + public static final int ACQUIRE_TOKEN = 1001; + public static final int ACQUIRE_TOKEN_SILENT = 1002; + public static final int INVALIDATE_ACCESS_TOKEN = 1003; + public static final int INVALIDATE_REFRESH_TOKEN = 1004; + public static final int INVALIDATE_FAMILY_REFRESH_TOKEN = 1006; + public static final int READ_CACHE = 1005; + + private Context mContext; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + mContext = getApplicationContext(); + + // Button for acquireToken call + final Button acquireTokenButton = (Button) findViewById(R.id.acquireToken); + acquireTokenButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + launchAuthenticationInfoActivity(ACQUIRE_TOKEN); + } + }); + + // Button for acquireTokenSilent call + final Button acquireTokenSilentButton = (Button) findViewById(R.id.acquireTokenSilent); + acquireTokenSilentButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + launchAuthenticationInfoActivity(ACQUIRE_TOKEN_SILENT); + } + }); + + final Button invalidateAccessTokenButton = (Button) findViewById(R.id.expireAccessToken); + invalidateAccessTokenButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + launchAuthenticationInfoActivity(INVALIDATE_ACCESS_TOKEN); + } + }); + + final Button invalidateRefreshToken = (Button) findViewById(R.id.invalidateRefreshToken); + invalidateRefreshToken.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + launchAuthenticationInfoActivity(INVALIDATE_REFRESH_TOKEN); + } + }); + + final Button invalidateFamilyRefreshToken = (Button) findViewById(R.id.invalidateFamilyRefreshToken); + invalidateFamilyRefreshToken.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + launchAuthenticationInfoActivity(INVALIDATE_FAMILY_REFRESH_TOKEN); + } + }); + + final Button readCacheButton = (Button) findViewById(R.id.readCache); + readCacheButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + processReadCacheRequest(); + } + }); + + final Button emptyCacheButton = (Button) findViewById(R.id.clearCache); + emptyCacheButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + processEmptyCacheRequest(); + } + }); + } + + private void launchAuthenticationInfoActivity(int flowCode) { + final Intent intent = new Intent(); + intent.setClass(mContext, SignInActivity.class); + intent.putExtra(FLOW_CODE, flowCode); + this.startActivity(intent); + } + + private void processEmptyCacheRequest() { + final AuthenticationContext authenticationContext = createAuthenticationContext(); + Intent intent = new Intent(); + try { + int numberOfCacheItem = getAllSerializedCacheItem(authenticationContext).size(); + authenticationContext.getCache().removeAll(); + + intent.putExtra(Constants.CLEARED_TOKEN_COUNT, String.valueOf(numberOfCacheItem)); + } catch (final JSONException e) { + intent = SignInActivity.getErrorIntentForResultActivity(Constants.JSON_ERROR, "Unable to convert to Json " + + e.getMessage()); + } + + launchResultActivity(intent); + } + + private void processReadCacheRequest() { + final AuthenticationContext authenticationContext = createAuthenticationContext(); + Intent intent = new Intent(); + try { + final ArrayList allItems = getAllSerializedCacheItem(authenticationContext); + intent.putStringArrayListExtra(Constants.READ_CACHE, allItems); + } catch (JSONException e) { + intent = SignInActivity.getErrorIntentForResultActivity(Constants.JSON_ERROR, "Unable to convert to Json " + + e.getMessage()); + } + + launchResultActivity(intent); + } + + private ArrayList getAllSerializedCacheItem(final AuthenticationContext authenticationContext) throws JSONException { + final ArrayList allItems = new ArrayList<>(); + final Iterator allCacheItemIterator = authenticationContext.getCache().getAll(); + final Gson gson = new GsonBuilder() + .registerTypeAdapter(Date.class, new DateTimeAdapter()) + .create(); + while (allCacheItemIterator.hasNext()) { + final TokenCacheItem item = allCacheItemIterator.next(); + final JSONObject tokenCacheItem = new JSONObject(); + tokenCacheItem.put(Constants.CACHE_DATA.ACCESS_TOKEN, item.getAccessToken()); + tokenCacheItem.put(Constants.CACHE_DATA.REFRESH_TOKEN, item.getRefreshToken()); + tokenCacheItem.put(Constants.CACHE_DATA.RESOURCE, item.getResource()); + tokenCacheItem.put(Constants.CACHE_DATA.AUTHORITY, item.getAuthority()); + tokenCacheItem.put(Constants.CACHE_DATA.CLIENT_ID, item.getClientId()); + tokenCacheItem.put(Constants.CACHE_DATA.RAW_ID_TOKEN, item.getRawIdToken()); + // unix timestamp + tokenCacheItem.put(Constants.CACHE_DATA.EXPIRES_ON, item.getExpiresOn().getTime() / 1000); + tokenCacheItem.put(Constants.CACHE_DATA.IS_MRRT, Boolean.toString(item.getIsMultiResourceRefreshToken())); + tokenCacheItem.put(Constants.CACHE_DATA.TENANT_ID, item.getTenantId()); + tokenCacheItem.put(Constants.CACHE_DATA.FAMILY_CLIENT_ID, item.getFamilyClientId()); + tokenCacheItem.put(Constants.CACHE_DATA.EXTENDED_EXPIRES_ON, item.getExtendedExpiresOn().getTime() / 1000); + + if (item.getUserInfo() != null) { + tokenCacheItem.put(Constants.CACHE_DATA.FAMILY_NAME, item.getUserInfo().getFamilyName()); + tokenCacheItem.put(Constants.CACHE_DATA.GIVEN_NAME, item.getUserInfo().getGivenName()); + tokenCacheItem.put(Constants.CACHE_DATA.UNIQUE_USER_ID, item.getUserInfo().getUserId()); + tokenCacheItem.put(Constants.CACHE_DATA.DISPLAYABLE_ID, item.getUserInfo().getDisplayableId()); + tokenCacheItem.put(Constants.CACHE_DATA.IDENTITY_PROVIDER, item.getUserInfo().getIdentityProvider()); + } + allItems.add(tokenCacheItem.toString()); + } + + return allItems; + } + + private AuthenticationContext createAuthenticationContext() { + // this is to clear or readthe whole cache, authority does not matter, use the common authority and clean up the whole cache. + final String authority = "https://login.microsoftonline.com/common"; + return new AuthenticationContext(mContext, authority, false); + } + + private void launchResultActivity(final Intent intent) { + intent.setClass(this.getApplicationContext(), ResultActivity.class); + this.startActivity(intent); + } +} \ No newline at end of file diff --git a/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/ResultActivity.java b/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/ResultActivity.java new file mode 100644 index 000000000..ed204f1b1 --- /dev/null +++ b/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/ResultActivity.java @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.microsoft.aad.automation.testapp; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.text.TextUtils; +import android.text.method.ScrollingMovementMethod; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import com.microsoft.aad.adal.ADALError; +import com.microsoft.aad.adal.Logger; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Date; + +/** + * Activity that is used to display the result sent back, could be success case or error case. + */ +public class ResultActivity extends AppCompatActivity { + + private TextView mTextView; + private TextView mLogView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_result); + + mTextView = (TextView) findViewById(R.id.resultInfo); + mTextView.setMovementMethod(new ScrollingMovementMethod()); + String resultText; + try { + resultText = convertIntentDataToJsonString(); + } catch (final JSONException e) { + resultText = "{\"error \" : \"Unable to convert to JSON\"}"; + } + mTextView.setText(resultText); + + mLogView = (TextView) findViewById(R.id.adalLogs); + mLogView.setText(((AndroidAutomationApp)this.getApplication()).getADALLogs()); + + final Button doneButton = (Button) findViewById(R.id.resultDone); + doneButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ResultActivity.this.finish(); + } + }); + } + + private String convertIntentDataToJsonString() throws JSONException { + final Intent intent = getIntent(); + + final JSONObject jsonObject = new JSONObject(); + + if (!TextUtils.isEmpty(intent.getStringExtra(Constants.ACCESS_TOKEN))) { + jsonObject.put(Constants.ACCESS_TOKEN, intent.getStringExtra(Constants.ACCESS_TOKEN)); + jsonObject.put(Constants.ACCESS_TOKEN_TYPE, intent.getStringExtra(Constants.ACCESS_TOKEN_TYPE)); + jsonObject.put(Constants.REFRESH_TOKEN, intent.getStringExtra(Constants.REFRESH_TOKEN)); + jsonObject.put(Constants.EXPIRES_ON, new Date(intent.getLongExtra(Constants.EXPIRES_ON, 0))); + jsonObject.put(Constants.TENANT_ID, intent.getStringExtra(Constants.TENANT_ID)); + jsonObject.put(Constants.UNIQUE_ID, intent.getStringExtra(Constants.UNIQUE_ID)); + jsonObject.put(Constants.DISPLAYABLE_ID, intent.getStringExtra(Constants.DISPLAYABLE_ID)); + jsonObject.put(Constants.FAMILY_NAME, intent.getStringExtra(Constants.FAMILY_NAME)); + jsonObject.put(Constants.GIVEN_NAME, intent.getStringExtra(Constants.GIVEN_NAME)); + jsonObject.put(Constants.IDENTITY_PROVIDER, intent.getStringExtra(Constants.IDENTITY_PROVIDER)); + jsonObject.put(Constants.ID_TOKEN, intent.getStringExtra(Constants.ID_TOKEN)); + } else if (!TextUtils.isEmpty(intent.getStringExtra(Constants.ERROR))) { + jsonObject.put(Constants.ERROR, intent.getStringExtra(Constants.ERROR)); + jsonObject.put(Constants.ERROR_DESCRIPTION, intent.getStringExtra(Constants.ERROR_DESCRIPTION)); + jsonObject.put(Constants.ERROR_CAUSE, intent.getSerializableExtra(Constants.ERROR_CAUSE)); + } else if (!TextUtils.isEmpty(intent.getStringExtra(Constants.EXPIRED_ACCESS_TOKEN_COUNT))) { + jsonObject.put(Constants.EXPIRED_ACCESS_TOKEN_COUNT, intent.getStringExtra(Constants.EXPIRED_ACCESS_TOKEN_COUNT)); + } else if (!TextUtils.isEmpty(intent.getStringExtra(Constants.INVALIDATED_REFRESH_TOKEN_COUNT))) { + jsonObject.put(Constants.INVALIDATED_REFRESH_TOKEN_COUNT, intent.getStringExtra(Constants.INVALIDATED_REFRESH_TOKEN_COUNT)); + } else if (!TextUtils.isEmpty(intent.getStringExtra(Constants.INVALIDATED_FAMILY_REFRESH_TOKEN_COUNT))) { + jsonObject.put(Constants.INVALIDATED_FAMILY_REFRESH_TOKEN_COUNT, intent.getStringExtra(Constants.INVALIDATED_FAMILY_REFRESH_TOKEN_COUNT)); + } else if (!TextUtils.isEmpty(intent.getStringExtra(Constants.CLEARED_TOKEN_COUNT))) { + jsonObject.put(Constants.CLEARED_TOKEN_COUNT, intent.getStringExtra(Constants.CLEARED_TOKEN_COUNT)); + } else if (intent.getStringArrayListExtra(Constants.READ_CACHE) != null) { + final ArrayList items = intent.getStringArrayListExtra(Constants.READ_CACHE); + jsonObject.put(Constants.ITEM_COUNT, items.size()); + + final ArrayList itemsWithCount = new ArrayList<>(); + itemsWithCount.addAll(items); + final JSONArray arrayItems = new JSONArray(itemsWithCount); + jsonObject.put("items", arrayItems); + } + + return jsonObject.toString(); + } +} diff --git a/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/SignInActivity.java b/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/SignInActivity.java new file mode 100644 index 000000000..266655e01 --- /dev/null +++ b/automationtestapp/src/main/java/com/microsoft/aad/automation/testapp/SignInActivity.java @@ -0,0 +1,432 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.microsoft.aad.automation.testapp; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import com.microsoft.aad.adal.ADALError; +import com.microsoft.aad.adal.AuthenticationCallback; +import com.microsoft.aad.adal.AuthenticationConstants; +import com.microsoft.aad.adal.AuthenticationContext; +import com.microsoft.aad.adal.AuthenticationException; +import com.microsoft.aad.adal.AuthenticationResult; +import com.microsoft.aad.adal.AuthenticationSettings; +import com.microsoft.aad.adal.CacheKey; +import com.microsoft.aad.adal.ITokenCacheStore; +import com.microsoft.aad.adal.Logger; +import com.microsoft.aad.adal.PromptBehavior; +import com.microsoft.aad.adal.TokenCacheItem; +import com.microsoft.aad.adal.UserInfo; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.UUID; + +/** + * Handle the coming request, will gather request info (JSON format of the data contains the authority, resource, clientId, + * redirect, ect). + */ +public class SignInActivity extends AppCompatActivity { + + public static final String AUTHORITY = "authority"; + public static final String RESOURCE = "resource"; + public static final String CLIENT_ID = "client_id"; + public static final String REDIRECT_URI = "redirect_uri"; + public static final String USE_BROKER = "use_broker"; + public static final String PROMPT_BEHAVIOR = "prompt_behavior"; + public static final String EXTRA_QUERY_PARAM = "extra_qp"; + public static final String VALIDATE_AUTHORITY = "validate_authority"; + public static final String USER_IDENTIFIER = "user_identifier"; + public static final String USER_IDENTIFIER_TYPE = "user_identifier_type"; + public static final String CORRELATION_ID = "correlation_id"; + + static final String INVALID_REFRESH_TOKEN = "some invalid refresh token"; + + private TextView mTextView; + private String mAuthority; + private String mResource; + private String mClientId; + private String mRedirectUri; + private boolean mUseBroker; + private PromptBehavior mPromptBehavior; + private String mLoginHint; + private String mUserId; + private String mExtraQueryParam; + private AuthenticationContext mAuthenticationContext; + private boolean mValidateAuthority; + private UUID mCorrelationId; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_request); + + mTextView = (EditText) findViewById(R.id.requestInfo); + + final Button goButton = (Button) findViewById(R.id.requestGo); + goButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + performAuthentication(); + } + }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + mTextView.setText(""); + mAuthenticationContext.onActivityResult(requestCode, resultCode, data); + } + + private void performAuthentication() { + final Intent receivedIntent = getIntent(); + + int flowCode = receivedIntent.getIntExtra(MainActivity.FLOW_CODE, 0); + + final Map inputItems; + try { + inputItems = readAuthenticationInfo(); + } catch (final JSONException e) { + sendErrorToResultActivity(Constants.JSON_ERROR, "Unable to read the input JSON info " + e.getMessage()); + return; + } + + if (inputItems.isEmpty()) { + return; + } + + validateUserInput(inputItems, flowCode); + + setAuthenticationData(inputItems); + AuthenticationSettings.INSTANCE.setUseBroker(mUseBroker); + + mAuthenticationContext = new AuthenticationContext(getApplicationContext(), mAuthority, mValidateAuthority); + switch (flowCode) { + case MainActivity.ACQUIRE_TOKEN: + acquireToken(); + break; + case MainActivity.ACQUIRE_TOKEN_SILENT: + acquireTokenSilent(); + break; + case MainActivity.INVALIDATE_ACCESS_TOKEN: + processExpireAccessTokenRequest(); + break; + case MainActivity.INVALIDATE_REFRESH_TOKEN: + processInvalidateRefreshTokenRequest(); + break; + case MainActivity.INVALIDATE_FAMILY_REFRESH_TOKEN: + processInvalidateFamilyRefreshTokenRequest(); + break; + default: + sendErrorToResultActivity("unknown_request", "Unknown request is received"); + break; + } + } + + static Intent getErrorIntentForResultActivity(final String error, final String errorDescription) { + final Intent intent = new Intent(); + intent.putExtra(Constants.ERROR, error); + intent.putExtra(Constants.ERROR_DESCRIPTION, errorDescription); + + return intent; + } + + private void sendErrorToResultActivity(final String error, final String errorDescription) { + launchResultActivity(getErrorIntentForResultActivity(error, errorDescription)); + } + + private void processExpireAccessTokenRequest() { + int count = expireAccessToken(); + final Intent intent = new Intent(); + intent.putExtra(Constants.EXPIRED_ACCESS_TOKEN_COUNT, String.valueOf(count)); + launchResultActivity(intent); + } + + private void processInvalidateRefreshTokenRequest() { + int count = invalidateRefreshToken(); + final Intent intent = new Intent(); + intent.putExtra(Constants.INVALIDATED_REFRESH_TOKEN_COUNT, String.valueOf(count)); + launchResultActivity(intent); + } + + private void processInvalidateFamilyRefreshTokenRequest() { + int count = invalidateFamilyRefreshToken(); + final Intent intent = new Intent(); + intent.putExtra(Constants.INVALIDATED_FAMILY_REFRESH_TOKEN_COUNT, String.valueOf(count)); + launchResultActivity(intent); + } + + private Map readAuthenticationInfo() throws JSONException { + final String userInputText = mTextView.getText().toString(); + if (TextUtils.isEmpty(userInputText)) { + // TODO: return error + sendErrorToResultActivity("empty_requestInfo", "No user input for the request."); + return Collections.emptyMap(); + } + + // parse Json response + final Map inputItems = new HashMap<>(); + extractJsonObjects(inputItems, userInputText); + + return inputItems; + } + + private static void extractJsonObjects(Map inputItems, String jsonStr) + throws JSONException { + final JSONObject jsonObject = new JSONObject(jsonStr); + final Iterator iterator = jsonObject.keys(); + + while (iterator.hasNext()) { + final String key = (String) iterator.next(); + inputItems.put(key, jsonObject.getString(key)); + } + } + + private void validateUserInput(final Map inputItems, int flowCode) { + if (inputItems.isEmpty()) { + throw new IllegalArgumentException("No sign-in data typed in the textBox"); + } + + if (TextUtils.isEmpty(inputItems.get(RESOURCE))) { + throw new IllegalArgumentException("resource"); + } + + if (TextUtils.isEmpty(inputItems.get(AUTHORITY))) { + throw new IllegalArgumentException("authority"); + } + + if (TextUtils.isEmpty(inputItems.get(CLIENT_ID))) { + throw new IllegalArgumentException("clientId"); + } + + if (flowCode == MainActivity.ACQUIRE_TOKEN && TextUtils.isEmpty(inputItems.get(REDIRECT_URI))) { + throw new IllegalArgumentException("redirect_uri"); + } + + if (flowCode == MainActivity.INVALIDATE_ACCESS_TOKEN && TextUtils.isEmpty(inputItems.get(USER_IDENTIFIER))) { + throw new IllegalArgumentException("user identifier"); + } + } + + private void setAuthenticationData(final Map inputItems) { + mAuthority = inputItems.get(AUTHORITY); + mResource = inputItems.get(RESOURCE); + mRedirectUri = inputItems.get(REDIRECT_URI); + mClientId = inputItems.get(CLIENT_ID); + mUseBroker = inputItems.get(USE_BROKER) == null ? false : Boolean.valueOf(inputItems.get(USE_BROKER)); + mPromptBehavior = getPromptBehavior(inputItems.get(PROMPT_BEHAVIOR)); + mExtraQueryParam = inputItems.get(EXTRA_QUERY_PARAM); + mValidateAuthority = inputItems.get(VALIDATE_AUTHORITY) == null ? true : Boolean.valueOf( + inputItems.get(VALIDATE_AUTHORITY)); + + if (!TextUtils.isEmpty(inputItems.get("unique_id"))) { + mUserId = inputItems.get("unique_id"); + } + + if (!TextUtils.isEmpty(inputItems.get("displayable_id")) || !TextUtils.isEmpty(inputItems.get("user_identifier"))) { + mLoginHint = inputItems.get("displayable_id") == null ? inputItems.get("user_identifier") : inputItems.get("displayable_id"); + } + + final String correlationId = inputItems.get(CORRELATION_ID); + if (!TextUtils.isEmpty(correlationId)) { + mCorrelationId = UUID.fromString(correlationId); + } + } + + PromptBehavior getPromptBehavior(final String inputPromptBehaviorString) { + if (TextUtils.isEmpty(inputPromptBehaviorString)) { + return null; + } + + if (inputPromptBehaviorString.equalsIgnoreCase(PromptBehavior.Always.toString())) { + return PromptBehavior.Always; + } else if (inputPromptBehaviorString.equalsIgnoreCase(PromptBehavior.Auto.toString())) { + return PromptBehavior.Auto; + } else if (inputPromptBehaviorString.equalsIgnoreCase(PromptBehavior.FORCE_PROMPT.toString())) { + return PromptBehavior.FORCE_PROMPT; + } else if (inputPromptBehaviorString.equalsIgnoreCase(PromptBehavior.REFRESH_SESSION.toString())) { + return PromptBehavior.REFRESH_SESSION; + } + + return null; + } + + private void acquireToken() { + mAuthenticationContext.acquireToken(SignInActivity.this, mResource, mClientId, + mRedirectUri, mLoginHint, mPromptBehavior, mExtraQueryParam, getAdalCallback()); + } + + private void acquireTokenSilent() { + mAuthenticationContext.acquireTokenSilentAsync(mResource, mClientId, mUserId, getAdalCallback()); + } + + private int expireAccessToken() { + final ITokenCacheStore tokenCacheStore = mAuthenticationContext.getCache(); + + int count = 0; + final String cacheKeyWithUserId = CacheKey.createCacheKeyForRTEntry(mAuthority, mResource, mClientId, mUserId); + final TokenCacheItem itemWithUserId = tokenCacheStore.getItem(cacheKeyWithUserId); + count += tokenExpired(itemWithUserId, cacheKeyWithUserId, tokenCacheStore); + + + final String cacheKeyWithDisplayableId = CacheKey.createCacheKeyForRTEntry(mAuthority, mResource, mClientId, mLoginHint); + final TokenCacheItem itemWithDisplayable = tokenCacheStore.getItem(cacheKeyWithDisplayableId); + count += tokenExpired(itemWithDisplayable, cacheKeyWithDisplayableId, tokenCacheStore); + + final String cacheKeyWithNoUser = CacheKey.createCacheKeyForRTEntry(mAuthority, mResource, mClientId, ""); + final TokenCacheItem itemWithNoUser = tokenCacheStore.getItem(cacheKeyWithNoUser); + count += tokenExpired(itemWithNoUser, cacheKeyWithNoUser, tokenCacheStore); + + return count; + } + + private int tokenExpired(final TokenCacheItem item, final String key, final ITokenCacheStore tokenCacheStore) { + final Calendar calendar = new GregorianCalendar(); + calendar.add(Calendar.HOUR, -2); + final Date expiredTime = calendar.getTime(); + + if (item != null && !TokenCacheItem.isTokenExpired(item.getExpiresOn())) { + item.setExpiresOn(expiredTime); + tokenCacheStore.setItem(key, item); + return 1; + } + + return 0; + } + + private int invalidateRefreshToken() { + expireAccessToken(); + + int count = 0; + // invalidate RT + count += invalidateRefreshToken(CacheKey.createCacheKeyForRTEntry(mAuthority, mResource, mClientId, mUserId)); + count += invalidateRefreshToken(CacheKey.createCacheKeyForRTEntry(mAuthority, mResource, mClientId, mLoginHint)); + count += invalidateRefreshToken(CacheKey.createCacheKeyForRTEntry(mAuthority, mResource, mClientId, "")); + + // invalidate MRRT + count += invalidateRefreshToken(CacheKey.createCacheKeyForMRRT(mAuthority, mClientId, mUserId)); + count += invalidateRefreshToken(CacheKey.createCacheKeyForMRRT(mAuthority, mClientId, mLoginHint)); + count += invalidateRefreshToken(CacheKey.createCacheKeyForMRRT(mAuthority, mClientId, "")); + + return count; + } + + private int invalidateFamilyRefreshToken() { + invalidateRefreshToken(); + + int count = 0; + // invalidate FRT + count += invalidateRefreshToken(CacheKey.createCacheKeyForFRT(mAuthority, AuthenticationConstants.MS_FAMILY_ID, mUserId)); + count += invalidateRefreshToken(CacheKey.createCacheKeyForFRT(mAuthority, AuthenticationConstants.MS_FAMILY_ID, mLoginHint)); + + return count; + } + + private int invalidateRefreshToken(final String key) { + final ITokenCacheStore tokenCacheStore = mAuthenticationContext.getCache(); + final TokenCacheItem item = tokenCacheStore.getItem(key); + + if (item != null) { + item.setRefreshToken(INVALID_REFRESH_TOKEN); + tokenCacheStore.setItem(key, item); + return 1; + } + + return 0; + } + + private AuthenticationCallback getAdalCallback() { + return new AuthenticationCallback() { + @Override + public void onSuccess(AuthenticationResult authenticationResult) { + final Intent intent = createIntentFromAuthenticationResult(authenticationResult); + launchResultActivity(intent); + } + + @Override + public void onError(Exception e) { + final Intent intent = createIntentFromReturnedException(e); + launchResultActivity(intent); + } + }; + } + + private Intent createIntentFromAuthenticationResult(final AuthenticationResult result) { + final Intent intent = new Intent(); + intent.putExtra(Constants.ACCESS_TOKEN, result.getAccessToken()); + intent.putExtra(Constants.REFRESH_TOKEN, result.getRefreshToken()); + intent.putExtra(Constants.ACCESS_TOKEN_TYPE, result.getAccessTokenType()); + intent.putExtra(Constants.EXPIRES_ON, result.getExpiresOn().getTime()); + intent.putExtra(Constants.TENANT_ID, result.getTenantId()); + intent.putExtra(Constants.ID_TOKEN, result.getIdToken()); + + if (result.getUserInfo() != null) { + final UserInfo userInfo = result.getUserInfo(); + intent.putExtra(Constants.UNIQUE_ID, userInfo.getUserId()); + intent.putExtra(Constants.DISPLAYABLE_ID, userInfo.getDisplayableId()); + intent.putExtra(Constants.GIVEN_NAME, userInfo.getGivenName()); + intent.putExtra(Constants.FAMILY_NAME, userInfo.getFamilyName()); + intent.putExtra(Constants.IDENTITY_PROVIDER, userInfo.getIdentityProvider()); + } + + return intent; + } + + private Intent createIntentFromReturnedException(final Exception e) { + final Intent intent = new Intent(); + if (!(e instanceof AuthenticationException)) { + intent.putExtra(Constants.ERROR, "unknown_exception"); + intent.putExtra(Constants.ERROR_DESCRIPTION, "unknown exception returned"); + } else { + final AuthenticationException authenticationException = (AuthenticationException) e; + intent.putExtra(Constants.ERROR, authenticationException.getCode().toString()); + intent.putExtra(Constants.ERROR_DESCRIPTION, authenticationException.getLocalizedMessage()); + intent.putExtra(Constants.ERROR_CAUSE, authenticationException.getCause()); + } + + return intent; + } + + private void launchResultActivity(final Intent intent) { + intent.putExtra(Constants.READ_LOGS, ((AndroidAutomationApp)this.getApplication()).getADALLogs()); + intent.setClass(this.getApplicationContext(), ResultActivity.class); + this.startActivity(intent); + this.finish(); + } +} diff --git a/automationtestapp/src/main/res/drawable/border.xml b/automationtestapp/src/main/res/drawable/border.xml new file mode 100644 index 000000000..5d25f0750 --- /dev/null +++ b/automationtestapp/src/main/res/drawable/border.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/automationtestapp/src/main/res/drawable/color_cursor.xml b/automationtestapp/src/main/res/drawable/color_cursor.xml new file mode 100644 index 000000000..06520aef1 --- /dev/null +++ b/automationtestapp/src/main/res/drawable/color_cursor.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/automationtestapp/src/main/res/layout/activity_main.xml b/automationtestapp/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..1df8e4c1f --- /dev/null +++ b/automationtestapp/src/main/res/layout/activity_main.xml @@ -0,0 +1,106 @@ + + + + + + + + +