diff --git a/CHANGELOG.md b/CHANGELOG.md index dcbf65a..6baa952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.0.0+2 +# 1.0.0+2 * Updates to readme in regards to kotlin static field issues. ## 1.0.0+1 * Added some more information to readme for clarity diff --git a/README.md b/README.md index deb6bc3..8abfee0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ e.g. `https://msalfluttertest.b2clogin.com/tfp/msalfluttertest.onmicrosoft.com/B For troubleshooting known bugs in the new build, please scroll down to the bottom of the page where all bugs and fixes we find will be noted. -# MSAL Wrapper Library for Flutter +## MSAL Wrapper Library for Flutter Please note this product is in very early alpha release and subject to change and bugs. The Microsoft Authentication Library Flutter Wrapper is a wrapper that uses that MSAL libraries for Android and IOS. Currently only the public client application functionality is supported, using the implicit workflow. @@ -37,6 +37,7 @@ This section is mostly copied and modified from [the official android MSAL libra ``` + ``` 2. In your AndroidManifest.xml file add the following intent filter, replacing the placeholder \ for your azure b2c application's client id where indicated below. @@ -53,6 +54,7 @@ The default redirect url is msal\://auth however this can now b android:host="auth" /> + ``` 3. Copy the [msal_default_config](https://raw.githubusercontent.com/moodio/msal-flutter/master/doc/templates/msal_default_config.json) from this repository (or make your own if you know what you're doing) and place it into your flutter apps android/src/main/res/raw folder. @@ -80,6 +82,7 @@ This section is mostly copied and modified from Step 1 from [the official iOS MS + ``` 2. Add LSApplicationQueriesSchemes to allow making call to Microsoft Authenticator if installed (For Authentication broker) @@ -90,6 +93,7 @@ This section is mostly copied and modified from Step 1 from [the official iOS MS msauthv2 msauthv3 + ``` 3. Open the app's iOS project in xcode, click on the Runner app to open up the configuration, and under capabilities, expand Keychain Sharing and add the keychain group `com.microsoft.adalcache` @@ -143,6 +147,7 @@ try{ } on MsalException { //error handling logic here } + ``` 4. Once a user has logged in atleast once, to retrieve a token silently call the acquireTokenSilent function, passing the scopes you wish to acquire the token for. Note that this function will throw an error on failure and should be surrounded by a try catch block as per the example below @@ -184,6 +189,9 @@ try{ # Trouble Shooting -Please note there is currently an issue that seems to occur with Android which uses slightly older versions of kotlin. -If you get the error when attemtping to acquire a token, along the lines of "static member msalApp not found", goto your app's android folder, open the build.gradle file, and on the second line change the version of kotlin from 1.3.10 to 1.3.50. For more information take a look at issue #4. +Please note there is currently an issue that seems to occur with Android which uses slightly older + versions of kotlin. +If you get the error when attemtping to acquire a token, along the lines of "static member msalApp +not found", goto your app's android folder, open the build.gradle file, and on the second line +change the version of kotlin from 1.3.10 to 1.3.50. For more information take a look at issue #4. A fix will be implemented shortly. \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index a844df8..1488f2c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -18,6 +18,11 @@ rootProject.allprojects { repositories { google() jcenter() + mavenCentral() + mavenLocal() + maven { + url 'https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1' + } } } @@ -26,22 +31,24 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { - compileSdkVersion 28 + compileSdkVersion 30 sourceSets { main.java.srcDirs += 'src/main/kotlin' } + defaultConfig { - minSdkVersion 21 + targetSdkVersion 30 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders = [msal_clientid: "5913dfb1-7576-451c-a7ea-a7c5a3f8682a"] } lintOptions { disable 'InvalidPackage' } + } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.microsoft.identity.client:msal:1.0.+' + implementation 'com.microsoft.identity.client:msal:2.0.2' } diff --git a/android/src/main/kotlin/uk/co/moodio/msal_flutter/MsalFlutterPlugin.kt b/android/src/main/kotlin/uk/co/moodio/msal_flutter/MsalFlutterPlugin.kt index c52dd26..b5fb084 100644 --- a/android/src/main/kotlin/uk/co/moodio/msal_flutter/MsalFlutterPlugin.kt +++ b/android/src/main/kotlin/uk/co/moodio/msal_flutter/MsalFlutterPlugin.kt @@ -4,58 +4,53 @@ import android.app.Activity import android.os.Handler import android.os.Looper import android.util.Log -import androidx.annotation.WorkerThread +import com.microsoft.identity.client.* +import com.microsoft.identity.client.IPublicClientApplication.IMultipleAccountApplicationCreatedListener +import com.microsoft.identity.client.IPublicClientApplication.LoadAccountsCallback +import com.microsoft.identity.client.exception.MsalClientException +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.exception.MsalServiceException +import com.microsoft.identity.client.exception.MsalUiRequiredException import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry.Registrar -import com.microsoft.identity.client.* -import com.microsoft.identity.client.exception.MsalException -import com.microsoft.identity.client.IPublicClientApplication -import com.microsoft.identity.client.PublicClientApplicationConfigurationFactory.initializeConfiguration @Suppress("SpellCheckingInspection") -class MsalFlutterPlugin: MethodCallHandler { - companion object - { - lateinit var mainActivity : Activity +class MsalFlutterPlugin : MethodCallHandler { + companion object { + lateinit var mainActivity: Activity lateinit var msalApp: IMultipleAccountPublicClientApplication + lateinit var accountList: List fun isClientInitialized() = ::msalApp.isInitialized @JvmStatic fun registerWith(registrar: Registrar) { - Log.d("MsalFlutter","Registering plugin") + // Log.d("MsalFlutter","Registering plugin") val channel = MethodChannel(registrar.messenger(), "msal_flutter") channel.setMethodCallHandler(MsalFlutterPlugin()) mainActivity = registrar.activity() } - fun getAuthCallback(result: Result) : AuthenticationCallback - { - Log.d("MsalFlutter", "Getting the auth callback object") - return object : AuthenticationCallback - { - override fun onSuccess(authenticationResult : IAuthenticationResult){ - Log.d("MsalFlutter", "Authentication successful") + fun getAuthCallback(result: Result): AuthenticationCallback { + + return object : AuthenticationCallback { + override fun onSuccess(authenticationResult: IAuthenticationResult) { Handler(Looper.getMainLooper()).post { result.success(authenticationResult.accessToken) } } - override fun onError(exception : MsalException) - { - Log.d("MsalFlutter","Error logging in!") - Log.d("MsalFlutter", exception.message) + override fun onError(exception: MsalException) { Handler(Looper.getMainLooper()).post { result.error("AUTH_ERROR", "Authentication failed", exception.localizedMessage) } } - override fun onCancel(){ - Log.d("MsalFlutter", "Cancelled") + override fun onCancel() { Handler(Looper.getMainLooper()).post { result.error("CANCELLED", "User cancelled", null) } @@ -63,15 +58,40 @@ class MsalFlutterPlugin: MethodCallHandler { } } - private fun getApplicationCreatedListener(result: Result) : IPublicClientApplication.ApplicationCreatedListener { - Log.d("MsalFlutter", "Getting the created listener") + /** + * Callback used in for silent acquireToken calls. + */ + fun getAuthSilentCallback(result: Result): SilentAuthenticationCallback { + return object : SilentAuthenticationCallback { + override fun onSuccess(authenticationResult: IAuthenticationResult) { + Log.d("MSAL_FLUTTER", "Successfully authenticated") + Handler(Looper.getMainLooper()).post { + result.success(authenticationResult.accessToken) + } + } + + override fun onError(exception: MsalException) { + /* Failed to acquireToken */ + Log.d("MSAL_FLUTTER", "Authentication failed: $exception") + if (exception is MsalClientException) { + result.error("NO_SCOPE", "Call must include a scope", exception.localizedMessage) + } else if (exception is MsalServiceException) { + result.error("NO_SCOPE", exception.localizedMessage, exception.localizedMessage) + } else if (exception is MsalUiRequiredException) { + result.error("NO_SCOPE", "Call must include a scope", exception.localizedMessage) + } + } + } + } + + private fun getApplicationCreatedListener(result: Result): IMultipleAccountApplicationCreatedListener { + + return object : IMultipleAccountApplicationCreatedListener { + override fun onCreated(application: IMultipleAccountPublicClientApplication) { + + msalApp = application + result.success(true) - return object : IPublicClientApplication.ApplicationCreatedListener - { - override fun onCreated(application: IPublicClientApplication) { - Log.d("MsalFlutter", "Created successfully") - msalApp = application as MultipleAccountPublicClientApplication - result.success(true) } override fun onError(exception: MsalException?) { @@ -80,51 +100,43 @@ class MsalFlutterPlugin: MethodCallHandler { } } } + + } - override fun onMethodCall(call: MethodCall, result: Result) - { - val scopesArg : ArrayList? = call.argument("scopes") + override fun onMethodCall(call: MethodCall, result: Result) { + val scopesArg: ArrayList? = call.argument("scopes") val scopes: Array? = scopesArg?.toTypedArray() - val clientId : String? = call.argument("clientId") - val authority : String? = call.argument("authority") + val clientId: String? = call.argument("clientId") + val authority: String? = call.argument("authority") - Log.d("MsalFlutter","Got scopes: $scopes") - Log.d("MsalFlutter","Got cleintId: $clientId") - Log.d("MsalFlutter","Got authority: $authority") - - when(call.method){ - "logout" -> Thread(Runnable{logout(result)}).start() + when (call.method) { + "logout" -> Thread(Runnable { logout(result) }).start() "initialize" -> initialize(clientId, authority, result) - "acquireToken" -> Thread(Runnable {acquireToken(scopes, result)}).start() - "acquireTokenSilent" -> Thread(Runnable {acquireTokenSilent(scopes, result)}).start() + "loadAccounts" -> Thread(Runnable { loadAccounts(result) }).start() + "acquireToken" -> Thread(Runnable { acquireToken(scopes, result) }).start() + "acquireTokenSilent" -> Thread(Runnable { acquireTokenSilent(scopes, result) }).start() else -> result.notImplemented() } } - private fun acquireToken(scopes : Array?, result: Result) - { - Log.d("MsalFlutter", "acquire token called") - + private fun acquireToken(scopes: Array?, result: Result) { // check if client has been initialized - if(!isClientInitialized()){ - Log.d("MsalFlutter","Client has not been initialized") + if (!isClientInitialized()) { Handler(Looper.getMainLooper()).post { result.error("NO_CLIENT", "Client must be initialized before attempting to acquire a token.", null) } } //check scopes - if(scopes == null){ - Log.d("MsalFlutter", "no scope") + if (scopes == null) { result.error("NO_SCOPE", "Call must include a scope", null) return } //remove old accounts - while(msalApp.accounts.any()){ - Log.d("MsalFlutter","Removing old account") + while (msalApp.accounts.any()) { msalApp.removeAccount(msalApp.accounts.first()) } @@ -132,21 +144,17 @@ class MsalFlutterPlugin: MethodCallHandler { msalApp.acquireToken(mainActivity, scopes, getAuthCallback(result)) } - private fun acquireTokenSilent(scopes : Array?, result: Result) - { - Log.d("MsalFlutter", "Called acquire token silent") - + private fun acquireTokenSilent(scopes: Array?, result: Result) { // check if client has been initialized - if(!isClientInitialized()){ - Log.d("MsalFlutter","Client has not been initialized") + + if (!isClientInitialized()) { Handler(Looper.getMainLooper()).post { result.error("NO_CLIENT", "Client must be initialized before attempting to acquire a token.", null) } } //check the scopes - if(scopes == null){ - Log.d("MsalFlutter", "no scope") + if (scopes == null) { Handler(Looper.getMainLooper()).post { result.error("NO_SCOPE", "Call must include a scope", null) } @@ -154,59 +162,89 @@ class MsalFlutterPlugin: MethodCallHandler { } //ensure accounts exist - if(msalApp.accounts.isEmpty()){ + if (accountList?.isEmpty()) { Handler(Looper.getMainLooper()).post { result.error("NO_ACCOUNT", "No account is available to acquire token silently for", null) } return } - + val selectedAccount: IAccount = accountList.first(); //acquire the token and return the result - val res = msalApp.acquireTokenSilent(scopes, msalApp.accounts[0], msalApp.configuration.defaultAuthority.authorityURL.toString()) - Handler(Looper.getMainLooper()).post { - result.success(res.accessToken) - } + val sc = scopes.map { s -> s.toLowerCase() }.toTypedArray() + + msalApp.acquireTokenSilentAsync(sc, selectedAccount, selectedAccount.authority, getAuthSilentCallback(result)) + } - private fun initialize(clientId: String?, authority: String?, result: Result) - { + + private fun initialize(clientId: String?, authority: String?, result: Result) { //ensure clientid provided - if(clientId == null){ - Log.d("MsalFlutter","error no clientId") + if (clientId == null) { result.error("NO_CLIENTID", "Call must include a clientId", null) return } //if already initialized, ensure clientid hasn't changed - if(isClientInitialized()){ - Log.d("MsalFlutter","Client already initialized.") - if(msalApp.configuration.clientId == clientId) - { + if (isClientInitialized()) { + if (msalApp.configuration.clientId == clientId) { result.success(true) } else { result.error("CHANGED_CLIENTID", "Attempting to initialize with multiple clientIds.", null) } } - - // if authority is set, create client using it, otherwise use default - if(authority != null){ - Log.d("MsalFlutter", "Authority not null") - Log.d("MsalFlutter", "Creating with: $clientId - $authority") - PublicClientApplication.create(mainActivity.applicationContext, clientId, authority, getApplicationCreatedListener(result)) - }else{ - Log.d("MsalFlutter", "Authority null") - PublicClientApplication.create(mainActivity.applicationContext, clientId, getApplicationCreatedListener(result)) + if(!isClientInitialized()) { + // if authority is set, create client using it, otherwise use default + PublicClientApplication.createMultipleAccountPublicClientApplication(mainActivity.applicationContext, + R.raw.msal_default_config, getApplicationCreatedListener(result)) } + } + /** + * Load currently signed-in accounts, if there's any. + */ + private fun loadAccounts(result: Result) { - private fun logout(result: Result){ - while(msalApp.accounts.any()){ - Log.d("MsalFlutter","Removing old account") - msalApp.removeAccount(msalApp.accounts.first()) + msalApp.getAccounts(object : LoadAccountsCallback { + + override fun onTaskCompleted(resultList: List) { + accountList = resultList + result.success(true) + } + + override fun onError(exception: MsalException) { + result.error("NO_ACCOUNT", "No account is available to acquire token silently for", exception) + } + }) + } + + + private fun logout(result: Result) { + if(!isClientInitialized()){ + Handler(Looper.getMainLooper()).post { + result.error("NO_ACCOUNT", "No account is available to acquire token silently for", null) + } + return } - Handler(Looper.getMainLooper()).post { - result.success(true) + + if (accountList?.isEmpty()) { + Handler(Looper.getMainLooper()).post { + result.error("NO_ACCOUNT", "No account is available to acquire token silently for", null) + } + return } + + msalApp.removeAccount(accountList.first(), object : IMultipleAccountPublicClientApplication.RemoveAccountCallback{ + override fun onRemoved() { + Thread(Runnable { loadAccounts(result) }).start() + } + + override fun onError(exception: MsalException) { + result.error("NO_ACCOUNT", "No account is available to acquire token silently for", exception) + } + }) + } } + + diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies new file mode 100644 index 0000000..13f0a16 --- /dev/null +++ b/example/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"msal_flutter","path":"/Users/fabioconceicao/Documents/Folder Projects/msal-flutter/","dependencies":[]}],"android":[{"name":"msal_flutter","path":"/Users/fabioconceicao/Documents/Folder Projects/msal-flutter/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"msal_flutter","dependencies":[]}],"date_created":"2020-11-11 08:48:46.235613","version":"1.22.3"} \ No newline at end of file diff --git a/example/android/app/Keystore/debug.keystore b/example/android/app/Keystore/debug.keystore new file mode 100644 index 0000000..cbcc215 Binary files /dev/null and b/example/android/app/Keystore/debug.keystore differ diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index b06baf9..26dc2a3 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion 30 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -38,9 +38,10 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "uk.co.moodio.msal_flutter_example" minSdkVersion 21 - targetSdkVersion 28 + targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -50,9 +51,24 @@ android { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. + buildConfigField "boolean", 'IS_PRINT_LOG', "false" + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.debug + } + + debug { signingConfig signingConfigs.debug } } + + signingConfigs { + debug { + storeFile file("Keystore/debug.keystore") + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } } flutter { @@ -61,7 +77,7 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' + testImplementation 'junit:junit:4.13' + androidTestImplementation 'androidx.test:runner:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' } diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index af28b19..cd515f2 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ ${e.message} --> ${e.code}"); throw _convertException(e); } } diff --git a/pubspec.yaml b/pubspec.yaml index 2a3903c..e6f62b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,8 @@ name: msal_flutter description: A Microsoft Authentication Library wrapper for Android and iOS -version: 1.0.0+2 -homepage: https://github.com/moodio/msal-flutter -repository: https://github.com/moodio/msal-flutter +version: 1.0.0+4 +#homepage: https://github.com/moodio/msal-flutter +#repository: https://github.com/moodio/msal-flutter environment: sdk: ">=2.1.0 <3.0.0"