Xamarin Forms Android - Authentication Failure on Deployment using MSAL Library - android

Development Information - Xamarin Forms Mobile application, utilizes MSAL library version 4.35.0 to authenticate against Azure AD and uses Brokered authentication flow utilizing the Microsoft Authenticator. This is coded in Visual Studio 2019 using C# and .Net 5.
Problem - Everything works in the Android emulator, but once deployed to an actual device, using Company Portal (Intune), the authentication piece fails with message:
Authentication Error [Android broker] The broker redirect URI is incorrect, it should be msauth://com.xxxxxx.xxxxxxx/xxxxxxxxxxxxxx Please visit https://aka.ms/Brokered-Authentication-for-Android for more details
I compared the redirect uri in the Azure portal to the one being displayed in the error message and they don't match, I don't know where it's getting this redirect uri value from?? Everything in the code base uses the callback uri specified in the Azure portal
I've worked through multiple MSDN documents, download example projects from GitHub, modified the Android Manifest file, etc. None of that seems to fix this issue. I am at my wits end with this. Here is an example of the Authentication code:
public static IPublicClientApplication PCA;
//OAuthSettings is a class containing my values to pass to the methods of the
//PublicClientApplicationBuilder
var builder = PublicClientApplicationBuilder
.Create(OAuthSettings.ApplicationId)
.WithTenantId(OAuthSettings.TenantId)
.WithBroker()
.WithRedirectUri(OAuthSettings.RedirectUri);
PCA = builder.Build();
try
{
var accounts = await PCA.GetAccountsAsync();
var silentAuthResult = await PCA
.AcquireTokenSilent(new string[] { "api://xxxxxxxxxxxxxx/.default" }, accounts.FirstOrDefault())
.ExecuteAsync();
AccessToken = new JwtSecurityToken(silentAuthResult.AccessToken);
//more code removed for brevity
}
catch (MsalUiRequiredException msalEx)
{
var windowLocatorService = DependencyService.Get<IParentWindowLocatorService>();
// Prompt the user to sign-in
var interactiveRequest = PCA.AcquireTokenInteractive(new string[] { "api://xxxxxxxxxxxxxxxxxxx/.default" });
//Used for Android and iOS
AuthUIParent = windowLocatorService?.GetCurrentParentWindow();
if (AuthUIParent != null)
{
interactiveRequest = interactiveRequest
.WithParentActivityOrWindow(AuthUIParent);
}
//
var interactiveAuthResult = await interactiveRequest.ExecuteAsync();
AccessToken = new JwtSecurityToken(interactiveAuthResult.AccessToken);
}
Android Manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="1.0" package="com.gpdgroup.GPDMobileAppTest" android:installLocation="auto" android:versionCode="7">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<application android:label="mycompany.Android" android:theme="#style/MainTheme" android:usesCleartextTraffic="true" android:icon="#mipmap/icon" android:roundIcon="#mipmap/icon">
<activity android:name="microsoft.identity.client.BrowserTabActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="msal{clientID}" android:host="auth" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="msauth" android:host="com.mycompany.myapp" android:path="/{base64 hash}" />
</intent-filter>
</activity>
</application>
<!--Necessary to fix issue on authentication for level 30-->
<queries>
<package android:name="com.azure.authenticator" />
<package android:name="com.mycompany.myapp" />
<package android:name="com.microsoft.windowsintune.companyportal" />
<!-- Required for API Level 30 to make sure the app detect browsers
(that don't support custom tabs) -->
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<!-- Required for API Level 30 to make sure the app can detect browsers that support custom tabs -->
<!-- https://developers.google.com/web/updates/2020/07/custom-tabs-android-11#detecting_browsers_that_support_custom_tabs -->
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
</manifest>
Screenshot of Azure Portal:
I also added an MSAL Authentication JSON file to the Resources folder of the Android project in a sub folder called raw called msal_default_config.json:
{
"client_id": "xxxxxxxxxxxxxxxxxxxxx",
"redirect_uri": "msauth://com.mycompany.myapp/{base64 url encoded signature hash}",
"broker_redirect_uri_registered": true,
"account_mode" : "SINGLE",
"authorities": [
{ "type": "AAD", "audience": { "type": "AzureADandPersonalMicrosoftAccount",
"tenant_id": "xxxxxxxxxxxxxxxxxx" }
} ]
}
And I also have this class for the Android project which inherits BrowserTabActivity class, called MsalActivity:
[Activity]
[IntentFilter(new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault },
DataHost = "auth",
DataScheme = "msal{clientID}")]
public class MsalActivity : BrowserTabActivity
{
}

The way Google has architected the mechanism through which one app can invoke another app mandates that the redirect URI has to include a signature of the application pkg.
The problem is that every Android SDK deployment has its own signature. So when you develop the app, you have one signature. When another developer tries to build and deploy, it's another signature. And when you create the official app that you submit, well there's another signature as well. They all use different Android SDKs.
SO you need to register 1 redirect URI for each app developer AND 1 redirect URI for the packaged bits.
You should expect that future versions of your application will have the same signature, so this process is done only once.

So after multiple changes and trying different things, we got it working, but we had to do the following:
I had my co-worker who manages the Play Store, create a new Application for release. (Not sure if this was necessary)
I changed the package name of the Android application to be all lower case. (Not sure if this was necessary)
Used the following to get the production signing key hash so that we could add it to the Azure portal. Production Signing Hash
How we got the production signature hash:
A. My co-worker who manages the Play Store, gave me the Hexadecmial SHA-1 value of the Application Signing from Google Play for the newly created application
B. Used the answer by Sujeet Kumar to get the base64 hash by feeding the Hexadecimal value into Chrome's console window with the following:
btoa('{your hexadecimal value goes here without the curly brackets}'.split(':')
.map(hc => String.fromCharCode(parseInt(hc, 16))).join(''))
C. Took this base64 hash and put it under the application Android callback uri section of the Azure portal
Since no one gave me this last step of how to get the production signature hash, I thought it may be helpful to put the steps I used to get it here.

Related

flutter: HttpException: Failed Forbidden from the twitter plugin

I'm trying to implement twitter login in my app but it doesn't work, returning an errorMessage in the AppResult object. Does anyone know a solution?
The packages I use are
twitter_login: ^4.2.3
firebase:
firebase_core: ^1.11.0
firebase_auth: ^3.3.5
Twitter config (User authentication settings page):
OAuth 1.0a enabled (is it the proper one for the plugin?)
Request email from users: disabled
App permissions: Read
Callback URI: https://project-name.firebaseapp.com/__/auth/handler
webiste url: https://www.google.com/
besides that everything is empty
Firebase config:
twitter auth enabled
api key set (checked it like 10 times)
api secret set (same thing)
Android manifest:
inside the activity tag:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with "example://gizmos” -->
<!-- Registered Callback URLs in TwitterApp -->
<data android:scheme="https" android:host="app-name.firebaseapp.com" />
<!-- host is option -->
</intent-filter>
after the activity tag:
<meta-data android:name="flutterEmbedding" android:value="2" />
The code itself:
final twitterLogin = TwitterLogin(
apiKey: '123 it's the same one',
apiSecretKey: 'proper one',
redirectURI: 'https://app-name.firebaseapp.com/__/auth/handler');
final authResult = await twitterLogin.login();
print(authResult.errorMessage); // prints out HttpException: Failed Forbidden
The code opens the link with the authentication, but after clicking on "authorize app", it returns to the app with the errorMessage "HttpException: Failed Forbidden"
Also, the authToken and the authTokenSecret are both null.
If you need any additional information, please let me know!
Your getting Forbidden http exception. So, in the official documentation it says -
The request is understood, but it has been refused or access is not allowed. An accompanying error message will explain why.
And the solution is given as -
Check that your developer account includes access to the endpoint you’re trying to use. You may also need to get your App allowlisted (e.g. Engagement API or Ads API) or sign up for access.
You can check - Twitter API Documentation
Hope it helps.
So, after a little bit of digging I found the answer to my question. In order to make it work I did the following:
changed the android scheme to appname://
removed the android host
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/>
<!-- Accepts URIs that begin with "example://gizmos” -->
<!-- Registered Callback URLs in TwitterApp -->
<data android:scheme="appname" />
<!-- host is option -->
</intent-filter>
changed the redirect url inside the twitter config to appname://
got elevated access for the twitter portal
used the loginV2 function along with OAuth2 instead of OAuth1
Future<UserCredential> _signInWithTwitter() async {
// Create a TwitterLogin instance
final twitterLogin = TwitterLogin(
apiKey: '123',
apiSecretKey: '1234',
redirectURI: 'appname://');
// Trigger the sign-in flow
final authResult = await twitterLogin.loginV2();
print(authResult.toMap());
// Create a credential from the access token
final twitterAuthCredential = TwitterAuthProvider.credential(
accessToken: authResult.authToken!,
secret: authResult.authTokenSecret!,
);
// Once signed in, return the UserCredential
return await FirebaseAuth.instance
.signInWithCredential(twitterAuthCredential);
}
Didn't use the callback provided by firebase at all (this is mentioned in the README too, but I'm too stupid to check)

How does Google Sign in for Android work without a redirect uri?

The Google Sign in library on Android works without specifying any redirect uri. Why is this the case? To which endpoint does Google send the access code to after the user logs in? And how does it redirect the user back to the app?
Thanks.
Now I see, the redirect uri is in fact the app itself, using a uri that points to a page on the app, not to any website. The redirect uri can be set up in the Android app by using the information here: https://developer.android.com/training/app-links/deep-linking. I learned a lot from this youtube video: https://www.youtube.com/watch?v=j3OTZ62AkNU
Once it redirects the user back to the app, the google sign in library handles getting the token and user info.
com.googleusercontent.apps.123:redirect_uri_path
com.example.app is the reverse DNS notation of a domain under your control. The custom scheme must contain a period to be valid.
com.googleusercontent.apps.123 is the reverse DNS notation of the client ID.
redirect_uri_path is an optional path component, such as /oauth2redirect. Note that the path should begin with a single slash, which is different from regular HTTP URLs.
^ Copied from documentation. 123 is your client id. And com.googleusercontent.apps is fixed, not variable. Setting this as the redirect uri in your app will make sure that google directs user back to your app, where the library will handle getting the access token and user profile, etc. You need to have an intent filter in your manifest.xml (or the following in Xamarin) to receive the uri.
[IntentFilter(
new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataSchemes = new[] { "com.googleusercontent.apps.123" },
DataPath = "/oauth2redirect")]
Its equivalent in the Manifest.xml:
<activity android:label="ActivityCustomUrlSchemeInterceptor" android:launchMode="singleTop" android:noHistory="true" android:name="crc640d96480bfe206cdf.ActivityCustomUrlSchemeInterceptor">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:path="/oauth2redirect" />
<data android:scheme="com.googleusercontent.apps.123" />
</intent-filter>
</activity>

Invalid Intent name for Google Actions

I am working to add Google Assistant actions in my app. When running the App Actions test tool, the error below keep poping-up and I do not know what to do.
App Actions Test Tool v3.1.5
Preview Creation Error
Status Code: 400
Message: Precondition check failed.
- Invalid intent name 'actions.intent.CHANGE_MY_SHIPMENT_DATE_BY_DATE'
- Invalid intent name 'actions.intent.CHANGE_MY_SHIPMENT_DATE_BY_DURATION'
- Invalid intent name 'actions.intent.CHANGE_MY_FREQUENCY'
- Invalid intent name 'actions.intent.ADD_FLOW'
- Invalid intent name 'actions.intent.ADD_TO_SUBSCRIPTION'
- Invalid intent name 'actions.intent.REMOVE_FLOW'
- Invalid intent name 'actions.intent.SNOOZE'
- Invalid intent name 'actions.intent.CUSTOMER_SERVICE'
- A parameter mapping is missing for the URL parameter 'box' in URL template 'content://com.xxyz.app.slices.provider/remove_flow{?product,box}' for intent 'actions.intent.REMOVE_FLOW'
actions.xml
<actions>
<action intentName="actions.intent.CHANGE_MY_SHIPMENT_DATE_BY_DATE">
<fulfillment
fulfillmentMode="actions.fulfillment.SLICE"
urlTemplate="content://com.xyz.app.slices.provider/change_shipment_by_date{?datetime}">
<parameter-mapping
intentParameter="datetimeType"
required="false"
urlParameter="datetime" />
</fulfillment>
</action>
...
<action intentName="actions.intent.CUSTOMER_SERVICE">
<fulfillment urlTemplate="https://xyz-app.firebaseapp.com/customer_service"/>
</action>
</actions>
I have followed the fitness app that google provide as example but it fails.
I have generated a aab file and uploaded it on the internal channel of the release management. I already have an app in production and I kept the same pkg name for this new app.
Also, I have added those intents in the DialogFlow and Actions console. The flow is working when testing on the cloud and also using the Google Assistant on my device. The GA recognise the dialog flow but nothing happened in the app. No slices or deeplink. I have added logs and none of the logs shows. Only the init below works:
#RequiresApi(Build.VERSION_CODES.P)
private fun grantAssistantPermissions() {
Timber.e(" [VOICEASSISTANT] grantAssistantPermissions")
getAssistantPackage()?.let { assistantPackage ->
val sliceProviderUri = Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(SLICE_AUTHORITY)
.build()
SliceManager.getInstance(this).grantSlicePermission(assistantPackage, sliceProviderUri)
}
}
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun getAssistantPackage(): String? {
val resolveInfoList = packageManager?.queryIntentServices(
Intent(VoiceInteractionService.SERVICE_INTERFACE), 0
)
return resolveInfoList?.firstOrNull()?.serviceInfo?.packageName
}
My manifest contain:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.app.slice.category.SLICE" />
</intent-filter>
</provider>
...
<!-- Define your supported deeplinks -->
<intent-filter
android:autoVerify="true"
tools:targetApi="m">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="xyz-app.firebaseapp.com"
android:scheme="https" />
</intent-filter>
<!-- Required to support search action intents from Google Search -->
<intent-filter>
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
Any idea why it keep complaining about bad intent name or even not react to any actions I ask ?
I have no clue to check if the actions is received by the app. I was expected an intent but nothing. As actions tool already raise errors, I assume that I can not expect the actions.xml to work as expected
Thanks for your help
App Actions and slices only work with the built-in Intents that are available for App Actions. They aren't available for custom conversational actions or conversational actions built using Dialogflow.

Cordova receive shared data from other app

At first please do not mar this question as duplicate. All other questions are old and I've tried solutions from most of them and none of them work.
I'm working on an Android app and I'd added my app to share Android menu. I'd like to add functionality so if user clicks my app on the share list e.g. in Chrome browser or Google Drive app, my app will receive the data from that app e.g. from Chrome it wold be URL.
I've tried to use different plugins and read many posts about how to use intent in my app. Unfortunately none of them work.
I've tried:
Sending url to ionic android app via webintents from another app - this one looked very promising
https://www.npmjs.com/package/phonegap-webintent
https://github.com/napolitano/cordova-plugin-intent - this one looked promising as well
https://github.com/okwei2000/webintent
https://github.com/stample/cordova-sharingreceptor
Did anyone got this working?
My AndroidManifest.xml is as follows - the part about intent:
<intent-filter android:label="#string/launcher_name">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
In my config.xml I have:
<preference name="AndroidLaunchMode" value="singleTask" />
And I'm not getting data I'm expecting.
When I'm using https://github.com/napolitano/cordova-plugin-intent plugin I'm getting the intent but clipItems element is missing and the plugin is useless.
What am I doing wrong?
Any help appreciated.
Finally I've done it.
I used https://github.com/napolitano/cordova-plugin-intent ver 0.1.3 and
window.plugins.intent.setNewIntentHandler(function (intent) {
// To get browser URL I had to use
var subject = intent.extras['android.intent.extra.SUBJECT'];
var url = intent.extras['android.intent.extra.TEXT'];
// For Chrome, Opera and FireFox it works. For build in Android browser TEXT holds page title and URL joined with +
});
Although ver 0.1.3 has
window.plugins.intent.getRealPathFromContentUrl(contentUrl, function (realPath) {}, function () {});
available I couldn't use it because this didn't give me the data I wanted. contentUrl is different for differen browsers e.g. for Chrome it is uri and for build in Android browser it's text.
I installed ver 0.1.3 using CLI
phonegap plugin add https://github.com/napolitano/cordova-plugin-intent.git#61a47ae8c28a62004eeff4ed96a6f3c64c271a16
I couldn't add it using tag in config.xml
I hope this will help others.

Facebook deep link never return data after app was installed in Android

I try to use FB deep link to get referral info after App installed when I clicked in deep link which was posted at FB. But I received deep link data only if app already installed.
As follow by this doc
https://developers.facebook.com/docs/applinks/android
Application should receive data from deep link, after app has been installed.
But native FB application send to GooglePlay only :
market://details?id=my.app.package&referrer=utm_source=apps.facebook.com&utm_campaign=fb4a&utm_content=%7B%22app%22%3A0%2C%22t%22%3A1436879844%7D
There is no info from deep link
And one first launch I try use in my launch screen next methods
AppLinks.getTargetUrlFromInboundIntent
and AppLinkData.fetchDeferredAppLinkData
but they get me null.
Step by step
I have created hosted api links for android. Where include all possible data for android https://developers.facebook.com/docs/graph-api/reference/v2.0/app/app_link_hosts
Then by FB SDK post this link.
Remove my application
Click on my post with deep link in native FB application
FB ask me to install app. And I have installed app from GooglePLay
Expected :
After install receive deep link data on start app.
But if use method described at FB Docs I did not receive any info
A heads up that I helped build Branch (branch.io), a linking tool that helps with deep linking through the Play Store. We've had a ton of trouble getting Facebook's methods to work reliably as well. If you use a Branch deep link to host your Facebook App Links, it works 100% of the time through page posts, ads and invites. We have a server to server integration with Facebook, but if that fails, we fall back to our fingerprinting mechanism where we match a browser fingerprint to a device fingerprint. (More on that here)
Here's a description of how to get setup so that Branch hosts your Facebook App Links and deep links through install:
Head to start.branch.io to get your Branch key and configure your link routes
Add in the io.branch.sdk.android library from Maven Central
Setup your manifest for deep linking. You'll start with this:
<application>
<!-- Other existing entries -->
<activity
android:name="com.yourapp.SplashActivity"
android:label="#string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
Add application subclass so that Branch can monitor for lifecycle changes to detect new deep links.
<application
android:name="io.branch.referral.BranchApp">
Add your intent filters to receive the deep link when the app is already installed
<intent-filter>
<data android:scheme="yourapp" android:host="open" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
Add your Branch key
<meta-data android:name="io.branch.sdk.BranchKey" android:value="your_key_here" />
The final Manifest should look like this:
<application
android:name="io.branch.referral.BranchApp">
<!-- Other existing entries -->
<activity
android:name="com.yourapp.SplashActivity"
android:label="#string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<data android:scheme="yourapp" android:host="open" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
<meta-data android:name="io.branch.sdk.BranchKey" android:value="your_key_here" />
</application>
Register to receive parameters from Branch deep link click. This is frequently put in onStart of the Activity that you want to use for deep link routing.
Branch branch = Branch.getInstance(getApplicationContext());
branch.initSession(new BranchReferralInitListener(){
#Override
public void onInitFinished(JSONObject referringParams, BranchError error) {
if (error == null) {
// params are the deep linked params associated with the link that the user clicked -> was re-directed to this app
// params will be empty if no data found
// ... insert custom logic here ...
} else {
Log.i("MyApp", error.getMessage());
}
}
}, this.getIntent().getData(), this);
Lastly, to you can create Branch-hosted Facebook App Links in 100 different ways. Here's how to create them dynamically from your app:
Branch branch = Branch.getInstance();
JSONObject obj = new JSONObject(); obj.putString("foo", "bar");
branch.getShortUrl(obj, "sms", "share", new BranchLinkCreateListener() {
#Override
public void onLinkCreate(String url, BranchError error) {
Log.i("MyApp", "Ready to share my link = " + url);
}
});
Happy linking!

Categories

Resources