From 50c05973963efda4bd5132e0364b84818bdb40bb Mon Sep 17 00:00:00 2001 From: Zak Barbuto Date: Fri, 4 Dec 2020 11:44:11 +1030 Subject: [PATCH 1/3] Add support for extra query parameters --- README.md | 46 ++++++--- .../FlutterMicrosoftAuthenticationPlugin.kt | 13 ++- example/lib/main.dart | 94 ++++++++++--------- ...FlutterMicrosoftAuthenticationPlugin.swift | 6 +- lib/flutter_microsoft_authentication.dart | 8 +- 5 files changed, 102 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 99d69ed..0aed6d7 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,14 @@ FlutterMicrosoftAuthentication fma = FlutterMicrosoftAuthentication( ); // Sign in interactively -String authToken = await this.fma.acquireTokenInteractively; +String authToken = await this.fma.acquireTokenInteractively(); + +// Alternatively - you can provide extra query extra +String authToken = await this.fma.acquireTokenInteractively({ + extraQueryParameters: { + 'domain_hint': 'login' + } +}); // Sign in silently String authToken = await this.fma.acquireTokenSilently; @@ -34,15 +41,17 @@ dependencies: ### Configuring MSAL for Android -| [Getting Started](https://docs.microsoft.com/azure/active-directory/develop/guidedsetups/active-directory-android)| [Library](https://github.com/AzureAD/microsoft-authentication-library-for-android) | [API Reference](http://javadoc.io/doc/com.microsoft.identity.client/msal) | [Support](README.md#community-help-and-support) -| --- | --- | --- | --- | +| [Getting Started](https://docs.microsoft.com/azure/active-directory/develop/guidedsetups/active-directory-android) | [Library](https://github.com/AzureAD/microsoft-authentication-library-for-android) | [API Reference](http://javadoc.io/doc/com.microsoft.identity.client/msal) | [Support](README.md#community-help-and-support) | +| ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------- | + +1. Register your app -1) Register your app - Create App Registration in Azure Portal - In Authentication, add Android platform and fill in your bundle id - Make note of the MSAL Configuration -2) Add BrowserTabActivity with RedirectUri to Android Manifest.xml +2. Add BrowserTabActivity with RedirectUri to Android Manifest.xml + ```xml @@ -57,7 +66,8 @@ dependencies: ``` -3) Create Msal Configuration JSON file +3. Create Msal Configuration JSON file + ```json { "client_id": "", @@ -78,7 +88,8 @@ dependencies: } ``` -4) Add android MSAL config file to pubspec.yaml assets +4. Add android MSAL config file to pubspec.yaml assets + ``` assets - assets/auth_config.json @@ -89,20 +100,24 @@ assets Library: https://github.com/AzureAD/microsoft-authentication-library-for-objc -1) Register your app +1. Register your app + - Create App Registration in Azure Portal - In Authentication, add iOS platform and fill in your bundle id - Make note of the MSAL Configuration -2) Add Keychain Sharing capability +2. Add Keychain Sharing capability + - In Xcode, under your applications Signing and Capabilities, add Keychain Sharing - Keychain Group should be `com.microsoft.adalcache` - Completely fine to have multiple Keychain Groups - This allows MSAL to use the keychain to share Microsoft Authentication sessions -3) Set up URL Schemes +3. Set up URL Schemes + - Add the following CFBundleURLTypes to your `Info.plist` file. - Remember to replace the bundle id. + ```xml CFBundleURLTypes @@ -115,8 +130,10 @@ https://github.com/AzureAD/microsoft-authentication-library-for-objc ``` -4) Allow MSAL to use Microsoft Authenticator if it is installed +4. Allow MSAL to use Microsoft Authenticator if it is installed + - Add the following LSApplicationQueriesSchemes to your `Info.plist` file. + ```xml LSApplicationQueriesSchemes @@ -125,8 +142,10 @@ https://github.com/AzureAD/microsoft-authentication-library-for-objc ``` -5) Handle the redirect callback +5. Handle the redirect callback + - Import MSAL + ```swift ... import MSAL @@ -141,5 +160,6 @@ https://github.com/AzureAD/microsoft-authentication-library-for-objc } ``` -6) Ensure that the minimum target is set to iOS 11 +6. Ensure that the minimum target is set to iOS 11 + - In Xcode, under General > Deployment info > Set the target to be no less than iOS 11 diff --git a/android/src/main/kotlin/za/co/britehouse/flutter_microsoft_authentication/FlutterMicrosoftAuthenticationPlugin.kt b/android/src/main/kotlin/za/co/britehouse/flutter_microsoft_authentication/FlutterMicrosoftAuthenticationPlugin.kt index 580de24..bd7b908 100644 --- a/android/src/main/kotlin/za/co/britehouse/flutter_microsoft_authentication/FlutterMicrosoftAuthenticationPlugin.kt +++ b/android/src/main/kotlin/za/co/britehouse/flutter_microsoft_authentication/FlutterMicrosoftAuthenticationPlugin.kt @@ -42,6 +42,7 @@ class FlutterMicrosoftAuthenticationPlugin: MethodCallHandler { val scopes: Array? = scopesArg?.toTypedArray() val authority: String? = call.argument("kAuthority") val configPath: String? = call.argument("configPath") + val extraQueryParameters: Map = call.argument("extraQueryParameters") ?: {}; if (configPath == null) { @@ -63,7 +64,7 @@ class FlutterMicrosoftAuthenticationPlugin: MethodCallHandler { } when(call.method){ - "acquireTokenInteractively" -> acquireTokenInteractively(scopes, authority, result) + "acquireTokenInteractively" -> acquireTokenInteractively(scopes, authority, extraQueryParameters, result) "acquireTokenSilently" -> acquireTokenSilently(scopes, authority, result) "loadAccount" -> loadAccount(result) "signOut" -> signOut(result) @@ -131,12 +132,18 @@ class FlutterMicrosoftAuthenticationPlugin: MethodCallHandler { }) } - private fun acquireTokenInteractively(scopes: Array, authority: String, result: Result) { + private fun acquireTokenInteractively(scopes: Array, authority: String, extraQueryParameters: Map , result: Result) { if (mSingleAccountApp == null) { result.error("MsalClientException", "Account not initialized", null) } - return mSingleAccountApp!!.signIn(mainActivity, "", scopes, getAuthInteractiveCallback(result)) + var parameterBuilder = AcquireTokenParameters.Builder() + .startAuthorizationFromActivity(mainActivity) + .withScopes(scopes.asList()) + .withAuthorizationQueryStringParameters(extraQueryParameters.map{ android.util.Pair(it.key, it.value) } ) + .withCallback((getAuthInteractiveCallback(result))); + var parameters = AcquireTokenParameters(parameterBuilder); + return mSingleAccountApp!!.acquireToken(parameters) } private fun acquireTokenSilently(scopes: Array, authority: String, result: Result) { diff --git a/example/lib/main.dart b/example/lib/main.dart index 2305bd2..15e4bdd 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -14,7 +14,6 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - String _graphURI = "https://graph.microsoft.com/v1.0/me/"; String _authToken = 'Unknown Auth Token'; @@ -28,19 +27,18 @@ class _MyAppState extends State { super.initState(); fma = FlutterMicrosoftAuthentication( - kClientID: "", - kAuthority: "https://login.microsoftonline.com/organizations", - kScopes: ["User.Read", "User.ReadBasic.All"], - androidConfigAssetPath: "assets/android_auth_config.json" - ); + kClientID: "", + kAuthority: "https://login.microsoftonline.com/organizations", + kScopes: ["User.Read", "User.ReadBasic.All"], + androidConfigAssetPath: "assets/android_auth_config.json"); print('INITIALIZED FMA'); } Future _acquireTokenInteractively() async { String authToken; try { - authToken = await this.fma.acquireTokenInteractively; - } on PlatformException catch(e) { + authToken = await this.fma.acquireTokenInteractively(); + } on PlatformException catch (e) { authToken = 'Failed to get token.'; print(e.message); } @@ -53,7 +51,7 @@ class _MyAppState extends State { String authToken; try { authToken = await this.fma.acquireTokenSilently; - } on PlatformException catch(e) { + } on PlatformException catch (e) { authToken = 'Failed to get token silently.'; print(e.message); } @@ -66,7 +64,7 @@ class _MyAppState extends State { String authToken; try { authToken = await this.fma.signOut; - } on PlatformException catch(e) { + } on PlatformException catch (e) { authToken = 'Failed to sign out.'; print(e.message); } @@ -83,9 +81,8 @@ class _MyAppState extends State { } _fetchMicrosoftProfile() async { - var response = await http.get(this._graphURI, headers: { - "Authorization": "Bearer " + this._authToken - }); + var response = await http.get(this._graphURI, + headers: {"Authorization": "Bearer " + this._authToken}); setState(() { _msProfile = json.decode(response.body).toString(); @@ -96,39 +93,46 @@ class _MyAppState extends State { Widget build(BuildContext context) { return MaterialApp( home: Scaffold( - appBar: AppBar( - title: const Text('Microsoft Authentication'), - ), - body: SingleChildScrollView( - child: Container( - width: double.infinity, - padding: EdgeInsets.all(8), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - RaisedButton( onPressed: _acquireTokenInteractively, - child: Text('Acquire Token'),), - RaisedButton( onPressed: _acquireTokenSilently, - child: Text('Acquire Token Silently')), - RaisedButton( onPressed: _signOut, - child: Text('Sign Out')), - RaisedButton( onPressed: _fetchMicrosoftProfile, - child: Text('Fetch Profile')), - if (Platform.isAndroid == true) - RaisedButton( onPressed: _loadAccount, - child: Text('Load account')), - SizedBox(height: 8,), - if (Platform.isAndroid == true) - Text( "Username: $_username"), - SizedBox(height: 8,), - Text( "Profile: $_msProfile"), - SizedBox(height: 8,), - Text( "Token: $_authToken"), - ], - ), + appBar: AppBar( + title: const Text('Microsoft Authentication'), ), - ) - ), + body: SingleChildScrollView( + child: Container( + width: double.infinity, + padding: EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + RaisedButton( + onPressed: _acquireTokenInteractively, + child: Text('Acquire Token'), + ), + RaisedButton( + onPressed: _acquireTokenSilently, + child: Text('Acquire Token Silently')), + RaisedButton(onPressed: _signOut, child: Text('Sign Out')), + RaisedButton( + onPressed: _fetchMicrosoftProfile, + child: Text('Fetch Profile')), + if (Platform.isAndroid == true) + RaisedButton( + onPressed: _loadAccount, child: Text('Load account')), + SizedBox( + height: 8, + ), + if (Platform.isAndroid == true) Text("Username: $_username"), + SizedBox( + height: 8, + ), + Text("Profile: $_msProfile"), + SizedBox( + height: 8, + ), + Text("Token: $_authToken"), + ], + ), + ), + )), ); } } diff --git a/ios/Classes/SwiftFlutterMicrosoftAuthenticationPlugin.swift b/ios/Classes/SwiftFlutterMicrosoftAuthenticationPlugin.swift index 2d9060d..b2d8674 100644 --- a/ios/Classes/SwiftFlutterMicrosoftAuthenticationPlugin.swift +++ b/ios/Classes/SwiftFlutterMicrosoftAuthenticationPlugin.swift @@ -26,7 +26,8 @@ public class SwiftFlutterMicrosoftAuthenticationPlugin: NSObject, FlutterPlugin msalView.onInit(clientId: clientId, redirectUri: redirectUri, scopes: scopes, authority: authority, flutterResult: result) if(call.method == "acquireTokenInteractively") { - msalView.acquireTokenInteractively(flutterResult: result) + let extraQueryParameters = dict["extraQueryParameters"] as? [String:String] ?? [:] + msalView.acquireTokenInteractively(extraQueryParameters: extraQueryParameters, flutterResult: result) } else if(call.method == "acquireTokenSilently") { msalView.acquireTokenSilently(flutterResult: result) } else if(call.method == "signOut") { @@ -109,13 +110,14 @@ extension ViewController { extension ViewController { - func acquireTokenInteractively(flutterResult: @escaping FlutterResult) { + func acquireTokenInteractively(extraQueryParameters: [String : String] = [:], flutterResult: @escaping FlutterResult) { guard let applicationContext = self.applicationContext else { return } guard let webViewParameters = self.webViewParamaters else { return } webViewParameters.webviewType = MSALWebviewType.wkWebView let parameters = MSALInteractiveTokenParameters(scopes: kScopes, webviewParameters: webViewParameters) parameters.promptType = .selectAccount; + parameters.extraQueryParameters = extraQueryParameters; applicationContext.acquireToken(with: parameters) { (result, error) in diff --git a/lib/flutter_microsoft_authentication.dart b/lib/flutter_microsoft_authentication.dart index a1d4359..4bee03c 100644 --- a/lib/flutter_microsoft_authentication.dart +++ b/lib/flutter_microsoft_authentication.dart @@ -38,9 +38,13 @@ class FlutterMicrosoftAuthentication { } /// Acquire auth token with interactive flow. - Future get acquireTokenInteractively async { + Future acquireTokenInteractively( + [Map options]) async { + options = options ?? {}; + final defaultArgs = _createMethodcallArguments(); + final methodCallArgs = {}..addAll(options..addAll(defaultArgs)); final String token = await _channel.invokeMethod( - 'acquireTokenInteractively', _createMethodcallArguments()); + 'acquireTokenInteractively', methodCallArgs); return token; } From 4fbac491537de1da039f9257a7ea34d1f82779f0 Mon Sep 17 00:00:00 2001 From: Zak Barbuto Date: Fri, 4 Dec 2020 12:49:33 +1030 Subject: [PATCH 2/3] Fix additional query parameter handling and add password reset error to Android --- .../FlutterMicrosoftAuthenticationPlugin.kt | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/android/src/main/kotlin/za/co/britehouse/flutter_microsoft_authentication/FlutterMicrosoftAuthenticationPlugin.kt b/android/src/main/kotlin/za/co/britehouse/flutter_microsoft_authentication/FlutterMicrosoftAuthenticationPlugin.kt index bd7b908..584d494 100644 --- a/android/src/main/kotlin/za/co/britehouse/flutter_microsoft_authentication/FlutterMicrosoftAuthenticationPlugin.kt +++ b/android/src/main/kotlin/za/co/britehouse/flutter_microsoft_authentication/FlutterMicrosoftAuthenticationPlugin.kt @@ -42,7 +42,7 @@ class FlutterMicrosoftAuthenticationPlugin: MethodCallHandler { val scopes: Array? = scopesArg?.toTypedArray() val authority: String? = call.argument("kAuthority") val configPath: String? = call.argument("configPath") - val extraQueryParameters: Map = call.argument("extraQueryParameters") ?: {}; + val extraQueryParameters: Map = call.argument("extraQueryParameters") ?: emptyMap(); if (configPath == null) { @@ -141,7 +141,7 @@ class FlutterMicrosoftAuthenticationPlugin: MethodCallHandler { .startAuthorizationFromActivity(mainActivity) .withScopes(scopes.asList()) .withAuthorizationQueryStringParameters(extraQueryParameters.map{ android.util.Pair(it.key, it.value) } ) - .withCallback((getAuthInteractiveCallback(result))); + .withCallback(getAuthInteractiveCallback(result)); var parameters = AcquireTokenParameters(parameterBuilder); return mSingleAccountApp!!.acquireToken(parameters) } @@ -190,14 +190,18 @@ class FlutterMicrosoftAuthenticationPlugin: MethodCallHandler { Log.d(TAG, "Authentication failed: ${exception.errorCode}") if (exception is MsalClientException) { - /* Exception inside MSAL, more info inside MsalError.java */ - Log.d(TAG, "Authentication failed: MsalClientException") - result.error("MsalClientException",exception.errorCode, null) - + /* Exception inside MSAL, more info inside MsalError.java */ + Log.d(TAG, "Authentication failed: MsalClientException") + result.error("MsalClientException",exception.errorCode, null) } else if (exception is MsalServiceException) { - /* Exception when communicating with the STS, likely config issue */ - Log.d(TAG, "Authentication failed: MsalServiceException") - result.error("MsalServiceException",exception.errorCode, null) + if(exception.message!!.contains("AADB2C90118")) { + Log.d(TAG, "Authentication failed: Forgot password") + result.error("FORGOT_PASSWORD_ERROR",exception.errorCode, null) + } else { + /* Exception when communicating with the STS, likely config issue */ + Log.d(TAG, "Authentication failed: MsalServiceException") + result.error("MsalServiceException", exception.errorCode, null) + } } } From 53fcdb2a211572b3c52c42d1d798c034e620fdfe Mon Sep 17 00:00:00 2001 From: zbarbuto Date: Wed, 9 Dec 2020 11:00:42 +1030 Subject: [PATCH 3/3] Always return a result to flutter from signOut Previously would just hang forever if not signed in --- .../SwiftFlutterMicrosoftAuthenticationPlugin.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ios/Classes/SwiftFlutterMicrosoftAuthenticationPlugin.swift b/ios/Classes/SwiftFlutterMicrosoftAuthenticationPlugin.swift index b2d8674..17efd7e 100644 --- a/ios/Classes/SwiftFlutterMicrosoftAuthenticationPlugin.swift +++ b/ios/Classes/SwiftFlutterMicrosoftAuthenticationPlugin.swift @@ -257,9 +257,17 @@ extension ViewController { */ func signOut(flutterResult: @escaping FlutterResult) { - guard let applicationContext = self.applicationContext else { return } + guard let applicationContext = self.applicationContext else { + flutterResult(FlutterError(code: "MISSING_APPLICATION", message: "Could not sign out without application context", details: nil)) + return + } - guard let account = self.currentAccount(flutterResult: flutterResult) else { return } + guard let account = self.currentAccount(flutterResult: flutterResult) else { + print("No active account to sign out of") + self.accessToken = "" + flutterResult(self.accessToken) + return + } do {