LoginManager in Xamarin iOS SDK doesn't cache AccessTokens - ios

I'm trying to use Xamarin native iOS library to authenticate with Facebook and access Graph API.
According to release 4.0.1.1 notes for the component (I didn't find any other documentation anywhere)
FBSDKTokenCachingStrategy. No alternative. LoginManager class caches
tokens to keychain automatically. You can observe token changes to do
manual post processing.
However this doesn't seem to be happening. When my iOS application starts I create LoginManager instance and call Init. However after that AccessToken.CurrentAccessToken is still null. It is only populated with data after I call LogInWithReadPermissionsAsync on the LoginManager.
Am I missing something or is it a bug.
Here's my code.
public bool IsLoggedIn
{
get
{
return AccessToken.CurrentAccessToken != null &&
AccessToken.CurrentAccessToken.ExpirationDate.ToDateTime() > DateTime.Now;
}
}
public Task<AccessToken> FacebookLoginInternal()
{
lock (monitor)
{
if (_loginTask == null)
{
LoginManager manager = new LoginManager();
manager.Init();
if (IsLoggedIn)
{
var ts = new TaskCompletionSource<AccessToken>();
ts.SetResult(AccessToken.CurrentAccessToken);
_loginTask = ts.Task;
}
else
{
var loginResult = manager.LogInWithReadPermissionsAsync(
new string[] { "email", "user_friends" });
_loginTask = loginResult.ContinueWith(r =>
{
return r.Result.Token;
});
}
}
return _loginTask;
}

As per response from Xamarin support (thank you!)
The following code fixes the issue:
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
return ApplicationDelegate.SharedInstance.FinishedLaunching(app, options);
}

This would seem to be expected behavior? You won't have a token until you log in, and this seems expected. I believe you may have misunderstood the note you pasted. It does not say that the token is cached as soon as you instantiate LoginManager or call Init on it, just that LoginManager will cache the token. It can't cache a token until a token is generated when you log in. That is why (I believe) Guilherme Torres Castro asked if the token is the same after a second call to LogInWithReadPermissionsAsync. If so, then the token was cached upon login.
Update: Communication with the OP via other channels indicates that I misunderstood. Log in is not maintained after app termination and relaunch, whereas in the native Obj-C Facebook iOS SDK it is. A bug has been filed: https://bugzilla.xamarin.com/show_bug.cgi?id=30287

Related

MSAL.NET OBO refresh token problems

I am trying to implement an OBO flow through to the graph API on a middle-tier API (.NET 5.0) using MSAL.NET. I'm running into two frustrating problems, and I can't find anyone having similar problems, so I think I'm misunderstanding something!
Problem 1: Whenever I call MSAL's GetAccountAsync, it always returns null when there should be an account loaded.
Problem 2: Whenever I call MSAL's AcquireTokenSilent, I always get the error "No refresh token found in the cache." even though I got one.
Here's what I have:
Once the web app authenticates, it passes through the token to a graph auth endpoint on the API:
var authenticationResult = await ClaimHelper.ClientApplication.AcquireTokenByAuthorizationCode(GraphHelpers.BasicGraphScopes, context.Code).ExecuteAsync();
var apiUserSession = await CouncilWiseAPIHelper.APIClient.Graph.AuthoriseUserAsync(authenticationResult.AccessToken);
which seems to work fine, and passes through a JWT to the API auth endpoint. The API implements an MSAL Confidential Client application and uses the SetBeforeAccess/SetAfterAccess token cache methods to save the cache to a database.
_msalClient = ConfidentialClientApplicationBuilder.Create(_graphConfig.ClientId)
.WithAuthority(AadAuthorityAudience.AzureAdMultipleOrgs)
.WithClientSecret(_graphConfig.ClientSecret)
.Build();
SetSerialiser(serialiser);
public void SetSerialiser(MSALTokenCacheSerialiser serialiser)
{
_msalClient.UserTokenCache.SetBeforeAccessAsync(serialiser.BeforeAccessCallbackAsync);
_msalClient.UserTokenCache.SetAfterAccessAsync(serialiser.AfterAccessCallbackAsync);
}
And the serialiser methods look like this:
public async Task BeforeAccessCallbackAsync(TokenCacheNotificationArgs notification)
{
GraphUserTokenCache tokenCache = await _graphUserTokenCacheRepository.GetByUserIdentifier(notification.SuggestedCacheKey);
if (tokenCache == null)
{
tokenCache = await _graphUserTokenCacheRepository.Get(notification.SuggestedCacheKey);
}
if (tokenCache != null)
{
notification.TokenCache.DeserializeMsalV3(tokenCache.Value);
}
}
public async Task AfterAccessCallbackAsync(TokenCacheNotificationArgs notification)
{
if (!notification.HasTokens)
{
// Delete from the cache
await _graphUserTokenCacheRepository.Delete(notification.SuggestedCacheKey);
}
if (!notification.HasStateChanged)
{
return;
}
GraphUserTokenCache tokenCache;
if (notification.SuggestedCacheKey == notification.Account.HomeAccountId.Identifier)
{
tokenCache = await _graphUserTokenCacheRepository.GetByUserIdentifier(notification.SuggestedCacheKey);
}
else
{
tokenCache = await _graphUserTokenCacheRepository.Get(notification.SuggestedCacheKey);
}
if (tokenCache == null)
{
var cache = notification.TokenCache.SerializeMsalV3();
tokenCache = new GraphUserTokenCache
{
Id = notification.SuggestedCacheKey,
AccountIdentifier = notification.Account.HomeAccountId.ToString(),
Value = cache
};
await _graphUserTokenCacheRepository.Add(tokenCache);
}
else
{
await _graphUserTokenCacheRepository.Update(tokenCache.Id, notification.TokenCache.SerializeMsalV3());
}
}
I can see the token BeforeAccess and AfterAccess methods being called, and I can see the caches being created in the database (encryption has been removed while I'm trying to track down this issue). If I inspect the serialised token cache being saved, it NEVER has a refresh token populated, but if I inspect the requests with fiddler I can see a refresh token was indeed provided.
Finally, here is the code for retrieving the access token which is called whenever a graph request is made:
public async Task<AuthenticationResult> GetAccessToken(string accountId, string jwtBearerToken)
{
try
{
IAccount account = null;
if (accountId.IsNotNullOrEmpty())
{
account = await _msalClient.GetAccountAsync(accountId);
}
var scope = _graphConfig.Scopes.Split(' ');
if (account == null)
{
var result = await _msalClient.AcquireTokenOnBehalfOf(scope,
new UserAssertion(jwtBearerToken))
.ExecuteAsync();
return result;
}
else
{
var result = await _msalClient.AcquireTokenSilent(scope, account)
.ExecuteAsync();
return result;
}
}
catch (MsalClientException ex)
{
ex.CwApiLog();
return null;
}
catch(Exception ex)
{
ex.CwApiLog();
return null;
}
}
When it's called with the jwtBearerToken, it will successfully call AcquireTokenOnBehalfOf() and the token is cached and a result returned, but when I come back to retrieve the account via GetAccountAsync() it always returns null even though I can see the token cache was loaded in BeforeAccessCallbackAsync().
Also, even if I call AcquireTokenSilent() immediately after acquiring the obo token with the account it just returned, I will get an exception saying there is no refresh token in the cache.
I am totally lost on what I'm doing wrong here, any help would be greatly appreciated.
I recently ran into the same problem while running a long runing OBO flow, MSAL has recently implemented an interface ILongRunningWebApi for these use cases you can go and see this new documentation
Here is an extract:
One OBO scenario is when a web API runs long running processes on
behalf of the user (for example, OneDrive which creates albums for
you). Starting with MSAL.NET 4.38.0, this can be implemented as such:
Before you start a long running process, call:
string sessionKey = // custom key or null
var authResult = await ((ILongRunningWebApi)confidentialClientApp)
.InitiateLongRunningProcessInWebApi(
scopes,
userToken,
ref sessionKey)
.ExecuteAsync();
userToken is a user token used to call this web API. sessionKey will
be used as a key when caching and retrieving the OBO token. If set to
null, MSAL will set it to the assertion hash of the passed-in user
token. It can also be set by the developer to something that
identifies a specific user session, like the optional sid claim from
the user token (for more information, see Provide optional claims to
your app). If the cache already contains a valid OBO token with this
sessionKey, InitiateLongRunningProcessInWebApi will return it.
Otherwise, the user token will be used to acquire a new OBO token from
AAD, which will then be cached and returned.
In the long-running process, whenever OBO token is needed, call:
var authResult = await ((ILongRunningWebApi)confidentialClientApp)
.AcquireTokenInLongRunningProcess(
scopes,
sessionKey)
.ExecuteAsync();
Pass the sessionKey which is associated with the current user's
session and will be used to retrieve the related OBO token. If the
token is expired, MSAL will use the cached refresh token to acquire a
new OBO access token from AAD and cache it. If no token is found with
this sessionKey, MSAL will throw a MsalClientException. Make sure to
call InitiateLongRunningProcessInWebApi first.
Hope this helps :)

Clearing Cookies In Xamarin iOS Build NHttpCookieStorage Not Clearing Cookies For WebView

Working on an app that uses Xamarin WebView, to run authentication. Trying to delete login info on user log out or cancel. Here is my code, mind the debugging that's going on. I already checked that the shared storage is empty, but something is going on because user login is still cached. After logging out, if the webpage is brought up again, the user is automatically signed in again. The code is running, as I am getting debugger output.
Code Snippet:
'
public void Clear()
{
NSHttpCookieStorage CookieStorage = NSHttpCookieStorage.SharedStorage;
foreach (var cookie in CookieStorage.Cookies)
{
Debug.WriteLine(cookie);
CookieStorage.DeleteCookie(cookie);
}
}
`
How I am Calling the Dependency Service
DependencyService.Get<IClearCookies>().Clear();
Modified a swift example to C# for a sort of "hacky" solution:
public void Clear()
{
NSHttpCookieStorage.SharedStorage.RemoveCookiesSinceDate(NSDate.DistantPast);
WKWebsiteDataStore.DefaultDataStore.FetchDataRecordsOfTypes(WKWebsiteDataStore.AllWebsiteDataTypes, (NSArray records) => {
for (nuint i = 0; i < records.Count; i++)
{
var record = records.GetItem<WKWebsiteDataRecord>(i);
WKWebsiteDataRecord[] recordArray = new WKWebsiteDataRecord[record.DataTypes.Count];
WKWebsiteDataStore.DefaultDataStore.RemoveDataOfTypes(record.DataTypes, NSDate.DistantPast, ()=> { });
}
});
}

How to know user is login by facebook or phone number in firebase ios (swift)? [duplicate]

I am using firebase from google and I have some trouble with user authentication. After logging with facebook I obtain FirebaseUser in AuthStateListener, but how can I detect if this user is logged via facebook or differently?
UPDATE
As #Frank van Puffelen said FirebaseAuth.getInstance().getCurrentUser().getProviderId()
should return "facebook", but in my case it returns "firebase". Now I cannot figure out what's the reason of this behavior. When I got FacebookToken I do something like this:
AuthCredential credential = FacebookAuthProvider.getCredential(facebookToken.getToken());
mAuth.signInWithCredential(credential)
.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
#Override
public void onComplete(#NonNull Task<AuthResult> task) {
// If sign in fails, display a message to the user. If sign in succeeds
// the auth state listener will be notified and logic to handle the
// signed in user can be handled in the listener.
if (!task.isSuccessful()) {
}
}
});
And afterthat before onComplete() method is called, my AuthStateListener gets user which provider id is not "facebook" as it should be. Am I doing something wrong? I followed official google documentation
In version 3.x and later a single user can be signed in with multiple providers. So there is no longer the concept of a single provider ID. In fact when you call:
FirebaseAuth.getInstance().getCurrentUser().getProviderId()
It will always return firebase.
To detect if the user was signed in with Facebook, you will have to inspect the provider data:
for (UserInfo user: FirebaseAuth.getInstance().getCurrentUser().getProviderData()) {
if (user.getProviderId().equals("facebook.com")) {
System.out.println("User is signed in with Facebook");
}
}
In my app, I use Anonymous Firebase accounts. When I connect Firebase auth with a Facebook account or Google Account I am checking like the following:
for (UserInfo user: FirebaseAuth.getInstance().getCurrentUser().getProviderData()) {
if (user.getProviderId().equals("facebook.com")) {
//For linked facebook account
Log.d("xx_xx_provider_info", "User is signed in with Facebook");
} else if (user.getProviderId().equals("google.com")) {
//For linked Google account
Log.d("xx_xx_provider_info", "User is signed in with Google");
}
}
For me, the following solution is working.
First, get the firebase user object if you have'nt already:
FirebaseAuth mAuth = FirebaseAuth.getInstance();
FirebaseUser firebaseUser = mAuth.getCurrentUser();
Now use the following on the FirebaseUser object to get the sign in provider:
firebaseUser.getIdToken(false).getResult().getSignInProvider()
Sources:
https://firebase.google.com/docs/reference/android/com/google/firebase/auth/FirebaseUser
https://firebase.google.com/docs/reference/android/com/google/firebase/auth/GetTokenResult.html
It will return password, google.com, facebook.com and twitter.com for email, google, facebook and twitter respectively.
Sharing for FirebaseAuth targeting version 6.x.x (Swift 5.0), year 2020:
I use Auth.auth().currentUser?.providerData.first?.providerID.
This will return password if logged in via email. And facebook.com if via Facebook.
There exist information in the responding Intent.
Refer to following snippet:
The responseCode is either "phone", "google.com", "facebook.com", or "twitter.com".
`import com.firebase.ui.auth.AuthUI;
import com.firebase.ui.auth.IdpResponse;
.....
#Override
protected void onActivityResult(final int requestCode, int resultCode, Intent
data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RC_SIGN_IN) {
progressBar.setVisibility(View.VISIBLE);
IdpResponse response = IdpResponse.fromResultIntent(data);
if (resultCode == RESULT_OK) {
String providerCode = response.getProviderType();
...
}
}
Most recent solution is:
As noted here
var uiConfig = {
callbacks: {
signInSuccessWithAuthResult: function(authResult, redirectUrl) {
var providerId = authResult.additionalUserInfo.providerId;
//...
},
//..
}
and for display in page
firebase.auth().onAuthStateChanged(function (user) {
if (user) {
user.getIdToken().then(function (idToken) {
$('#user').text(welcomeName + "(" + localStorage.getItem("firebaseProviderId")+ ")");
$('#logged-in').show();
}
}
});

Unity3D iOS device token is always null

I am trying to get push notifications to work on iOS, but I can not get access to the device token!
My Unity version is 5.4.1f1.
I have enabled the Push Notifications capability in XCode and all the certificates are setup correctly:
In my script in the start method I call this:
UnityEngine.iOS.NotificationServices.RegisterForNotifications
(UnityEngine.iOS.NotificationType.Alert | UnityEngine.iOS.NotificationType.Badge
| UnityEngine.iOS.NotificationType.Sound, true);
Then from the update method I call this method:
private bool RegisterTokenWithPlayfab( System.Action successCallback,
System.Action<PlayFabError> errorCallback )
{
byte[] token = UnityEngine.iOS.NotificationServices.deviceToken;
if(token != null)
{
// Registration on backend
}
else
{
string errorDescription = UnityEngine.iOS.NotificationServices.registrationError;
Debug.Log( "Push Notifications Registration failed with: " + errorDescription );
return false;
}
}
The token keeps being empty, so the else-branch is entered every call. Also, the registrationError keeps being empty.
Can someone point me in the right direction on this? What else can I try or how can I get more infos on what is going wrong??
Try this one
Go to your application target. Choose Capabilities and ensure that ‘Push Notifications’ is enabled there.
You need to check deviceToken in a Coroutine or Update.
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using NotificationServices = UnityEngine.iOS.NotificationServices;
using NotificationType = UnityEngine.iOS.NotificationType;
public class NotificationRegistrationExample : MonoBehaviour
{
bool tokenSent;
void Start()
{
tokenSent = false;
NotificationServices.RegisterForNotifications(
NotificationType.Alert |
NotificationType.Badge |
NotificationType.Sound, true);
}
void Update()
{
if (!tokenSent)
{
byte[] token = NotificationServices.deviceToken;
if (token != null)
{
// send token to a provider
string token = System.BitConverter.ToString(token).Replace('-', '%');
Debug.Log(token)
tokenSent = true;
}
}
}
}
The implementation is horrible, but we don't have callbacks from Unity side so we need to keep listening that variable value.
Check the documentation:
https://docs.unity3d.com/ScriptReference/iOS.NotificationServices.RegisterForNotifications.html
Also seems to need internet. I guess there is an Apple service going on there.
Yes, registration error is empty even when user deniy permissions.
What i did is to use UniRX and set an Observable fron a Coroutine with a time out so it dont keep forever asking for it.
If you accepted and you do not received the token might be the internet conection. And i guess you are teying this on a real device.

Xamarin Forms does not wait for GeoLocation approval on iOS

I am have Xamarin Forms cross platform application for iOS, Android and UWP. I use the Xam.Plugin.Geolocator to get the location from each of the devices. My challenge with iOS is on the first launch of the app on a device. My code runs through and detects that IsGeolocationEnabled for the Plugin.Geolocator.Abstractions.IGeolocator object is false before the use is ever presented with the option to allow the application to use the device's location. This causes my app to inform the user that Location Services are not enabled for the application.
Basically I am hitting the line of code below before the use is ever asked about location services:
if (!App.gobj_RealGeoCoordinator.IsGeolocationEnabled)
ls_ErrorMessage = resourcestrings.GetValue("NoLocationServicesMessage");
On the other platforms, UWP at least, it seems that the app is paused while waiting for the user to respond to the request to use location services. Android just seems to automatically allow access to location if an app uses it.
Any idea how I can have the iOS detect if the request to use location services has been answered or not on the first run? Any suggestions would be greatly appreciated.
UPDATE(1):
I have all the correct items in my info.plist as seen below. I do eventually get the request to use the location just after my app has already checked IsGeolocationEnabled and decided the user has not enabled location services for the app.
UPDATE (2):
So I made a little progress using the following code.
try
{
while (!App.gobj_RealGeoCoordinator.IsGeolocationEnabled)
{
await Task.Delay(1000);
}
ViewModelObjects.AppSettings.CanAccessLocation = App.gobj_RealGeoCoordinator.IsGeolocationEnabled;
}
catch (Exception ex)
{
XXXXXXX
}
The challenge is that the plugin appears to provide me no way of knowing in the user has not responded to the location services dialog (i.e. IsGeolocationEnabled == false) versus the user said no to the location services dialog (also IsGeolocationEnabled == false). Any suggestions?
The way this type of permission request occurs on iOS is through an asynchronous dialog prompt, which is only shown if needed (and not until it is needed). Basically, you need to set up a callback from the CLLocation API. I have a helper class that I use for this purpose, which makes it even easier. Just call GetCurrentDeviceLocation() and pass it a callback function. The callback will only be invoked once the user has granted permission to the app, or if they previously granted permission:
public class GeoLocationService
{
readonly CLLocationManager _locationManager;
WeakReference<Action<Position>> _callback;
public GeoLocationService()
{
_locationManager = new CLLocationManager ();
_locationManager.AuthorizationChanged += AuthorizationChanged;
}
void AuthorizationChanged (object sender, CLAuthorizationChangedEventArgs e)
{
Action<Position> callback;
if (_callback == null || !_callback.TryGetTarget (out callback)) {
return;
}
if (IsAuthorized(e.Status)) {
var loc = _locationManager.Location;
var pos = new Position(loc.Coordinate.Latitude, loc.Coordinate.Longitude);
callback (pos);
}
}
static bool IsAuthorized(CLAuthorizationStatus status)
{
return
status == CLAuthorizationStatus.Authorized
|| status == CLAuthorizationStatus.AuthorizedAlways
|| status == CLAuthorizationStatus.AuthorizedWhenInUse;
}
public void GetCurrentDeviceLocation (Action<Position> callback)
{
_callback = new WeakReference<Action<Position>> (callback);
if (UIDevice.CurrentDevice.CheckSystemVersion (8, 0)) {
if (_locationManager.Location == null) {
_locationManager.RequestWhenInUseAuthorization ();
return;
}
}
AuthorizationChanged (null, new CLAuthorizationChangedEventArgs (CLAuthorizationStatus.Authorized));
}
}

Resources