IWA Portal Login and Feature Service Access

2578
12
03-14-2019 11:26 AM
AndyWright
Occasional Contributor

We've got an IWA Portal setup in house and we are working on getting our iOS Runtime Xamarin Forms application to work properly with it.  We have been using oAuth based authentication for years and now have customers implementing IWA based Portals and need to support that.  

To get the login going I am using the ArcGISNetworkCredential.  When the authentication challenge is issued against our IWA portal we pop up a home grown username/password form, take that information, create the ArcGISNetworkCredential and login to the portal accordingly.  I've got that all working well.

We have a couple of tools that utilize feature services to create offline geodatabases to support field work.  When I try to generate those offline geodatabases using the ArcGISNetworkCredential I am getting an error that a token is required.  Makes sense.  Problem is the ArcGISNetworkCredential does not have a token associated with it. 

I then switched gears and attempted to use the ArcGISTokenCredential for everything - logging into the portal and generating geodatabases from feature services.  No luck there - could not get that to work for logging in.

I've been through the multitude of online documents referring to integrated windows authentication implementation and have used a lot of code examples in there within our app.  I have not found anything that addresses our particular scenario though.  I feel like I'm close on this and am missing a small piece of the puzzle.

Can someone point me in the right direction?  Thanks!

0 Kudos
12 Replies
JoeHershman
MVP Regular Contributor

I'd be curious how you get the Windows user name from an iOS device, but that is another question.

When you use the Windows user name I have found that the user name needs to be in the form:

user@DOMAIN where domain is in all caps.  If using Windows authentication and the domain is not in CAPS it fails

Thanks,
-Joe
0 Kudos
AndyWright
Occasional Contributor

We pop up a homegrown login page for the user to enter their creds.  That works fine.  My issue is with accessing a feature service on our portal and making a request to that service.  It is complaining about a token when I make that request to the service.

0 Kudos
JoeHershman
MVP Regular Contributor

Can you post the code?

Thanks,
-Joe
0 Kudos
AndyWright
Occasional Contributor

That's a little tough as we've got pieces of all this all over the place, but I'll get you the basics here in no specific order.  What I was after with this post was a recipe.  There has to be a set of steps that has to be taken to log into an IWA portal and be able to make requests against feature services in that portal.  It seems like the ArcGISNetworkCredential gets me in the front door, but doesn't allow me to drink any beers in the fridge if you know what I mean.  

AuthenticationManager.Current.ChallengeHandler = new IwaChallengeHandler { AllowSaveCredentials = saveCredentials };

await CreateIwaCredentialAsync(new CredentialRequestInfo
 {
     ServiceUri = new Uri(_serviceUri),
     AuthenticationType = AuthenticationType.NetworkCredential
 });

ArcGISNetworkCredential credential = await 
    AuthenticationManager.Current.ChallengeHandler.CreateCredentialAsync(credRequestInfo) 
    as ArcGISNetworkCredential;

if (credRequestInfo.Response == null)
       AuthenticationManager.Current.AddCredential(credential);
‍‍‍‍‍‍‍‍‍‍‍‍

using EFCloud.Events;
using EFCloud.Security.Managers;
using EFCloud.ViewModels;
using EFCloud.Views;
using Esri.ArcGISRuntime.Security;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace EFCloud.Security.Handlers
{
	internal class IwaChallengeHandler : IChallengeHandler
	{
		#region Variable/property declaration

		private bool _allowSaveCredentials;
		private bool _areCredentialsRestored;
		private LoginPage _loginPage = new LoginPage();


		// A TaskCompletionSource to store the result of a login task.
		private TaskCompletionSource<Credential> _loginTaskCompletionSrc;


		public bool AllowSaveCredentials
		{
			//  The first time AllowSaveCredentials is set to true, the cached credentials are added to the AuthenticationManager
			//  The default value is false

			get { return _allowSaveCredentials; }

			set
			{
                if (_allowSaveCredentials != value)
                {
                    _allowSaveCredentials = value;

                    if (_allowSaveCredentials && !_areCredentialsRestored)
                    {
                        //  The first time AllowSaveCredentials is set to true, add the cached credentials to AuthenticationManager

                        _areCredentialsRestored = true;

                        foreach (var crd in RetrieveAllSavedCredentials())
                        {
                            AuthenticationManager.Current.AddCredential(crd);
                        }
                    }
                }
            }
		}

		#endregion

		#region Constructor and Initialize

		public IwaChallengeHandler()
		{
			Initialize();
		}

		private void Initialize()
		{
			// Set up event handlers for when the user completes the login entry or cancels
			LoginPageViewModel loginPageViewModel = _loginPage.BindingContext as LoginPageViewModel;
			loginPageViewModel.OnLoginInfoEntered += LoginInfoEntered;
			loginPageViewModel.OnCanceled += LoginCanceled;
		}

        #endregion


        /// <summary>
        /// Retrieves all EpochField credentials
        /// </summary>
        /// <returns>IEnumerable<Credential></returns>
        internal IEnumerable<Credential> RetrieveAllSavedCredentials()
        {
            return CredentialManager.RetrieveAll();
        }

        public async Task<Credential> CreateCredentialAsync(CredentialRequestInfo info)
		{
			// Ignore challenges for OAuth (might come from secured layers in public web maps, for example).
			if (info.AuthenticationType != AuthenticationType.NetworkCredential)
			{
				Console.WriteLine("Authentication for " + info.ServiceUri.Host + " skipped.");
				return null;
			}

			// Return if authentication is already in process
			if (_loginTaskCompletionSrc != null && !_loginTaskCompletionSrc.Task.IsCanceled)
			{
				return null;
			}

			// Create a new TaskCompletionSource for the login operation
			// (passing the CredentialRequestInfo object to the constructor will make it available from its AsyncState property)
			_loginTaskCompletionSrc = new TaskCompletionSource<Credential>(info);

            // Show the login controls on the UI thread
            // OnLoginInfoEntered event will return the values entered (username, password, and domain)

            ArcGISNetworkCredential iwaCred = AuthenticationManager.Current.FindCredential(info.ServiceUri, AuthenticationType.NetworkCredential) as ArcGISNetworkCredential;

            if (iwaCred != null)
            {
                _loginTaskCompletionSrc.TrySetResult(iwaCred);
            }
			else
            {
                Device.BeginInvokeOnMainThread(async () => await App.Current.MainPage.Navigation.PushAsync(_loginPage));
            }

			// Return the login task, the result will be ready when completed (user provides login info and clicks the "Login" button)
			return await _loginTaskCompletionSrc.Task;
		}

		#region Event Handlers

		// Handle the OnLoginEntered event from the login UI
		// LoginEventArgs contains the username, password, and domain that were entered
		private void LoginInfoEntered(object sender, LoginDialogEventArgs e)
		{
			// Make sure the task completion source has all the information needed
			if (_loginTaskCompletionSrc == null ||
				_loginTaskCompletionSrc.Task == null ||
				_loginTaskCompletionSrc.Task.AsyncState == null)
			{
				return;
			}

			try
			{
				// Get the associated CredentialRequestInfo (will need the URI of the service being accessed)
				CredentialRequestInfo requestInfo = _loginTaskCompletionSrc.Task.AsyncState as CredentialRequestInfo;

				// Create a new network credential using the values entered by the user
				var networkCredential = new System.Net.NetworkCredential(e.Username, e.Password, e.Domain);

				// Create a new ArcGIS network credential to hold the network credential and service URI
				var arcgisCredential = new ArcGISNetworkCredential
				{
					Credentials = networkCredential,
					ServiceUri = requestInfo.ServiceUri
				};

                if (AllowSaveCredentials)
                {
                    CredentialManager.AddCredential(arcgisCredential);
                }

                // Set the task completion source result with the ArcGIS network credential
                // AuthenticationManager is waiting for this result and will add it to its Credentials collection
                _loginTaskCompletionSrc.TrySetResult(arcgisCredential);
			}
			catch (Exception ex)
			{
				// Unable to create credential, set the exception on the task completion source
				_loginTaskCompletionSrc.TrySetException(ex);
			}
			finally
			{
				// Dismiss the login controls
				App.Current.MainPage.Navigation.PopAsync();
			}
		}

		private void LoginCanceled(object sender, EventArgs e)
		{
			// Dismiss the login controls
			App.Current.MainPage.Navigation.PopAsync();

			// Cancel the task completion source task
			_loginTaskCompletionSrc.TrySetCanceled();
		}

		#endregion

		public void Reset()
		{
			_loginTaskCompletionSrc = null;
		}
	}
}
0 Kudos
JoeHershman
MVP Regular Contributor

Certainly a lot more involved than what I have ever done.  This is the very basic approach when I save a User/Password in configuration so things work on machines not attached to the domain (mainly because my test environment is often not on the domain of the Portal but the Portal uses Windows Auth)

private async Task<TokenCredential> GetWindowsCredential()
{
	Uri sharingUri = new Uri($"{Settings.PortalUrl}/sharing/rest");
	TokenCredential credential;

	try
	{
		//Assume that IWA is turned on on machine
		credential = await AuthenticationManager.Current.GenerateCredentialAsync(sharingUri, "", "");
	}
	catch (Exception)
	{
		//if failed then use provided user and password - testing also requires
		// user name muust be in form: user@DOMAINNAME
		credential = await AuthenticationManager.Current.GenerateCredentialAsync(sharingUri, Settings.WindowsUser, Settings.WindowsPwd);
	}

	return credential;
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

I do the same in another app (it is WPF not Xamarin though) that uses a form to get user input.  All done basically the same except for a little ditty to get the password:

// _securePassword is SecureString
string password = new NetworkCredential(string.Empty, _securePassword).Password;‍‍‍

In WPF when the user enters into a password text box it gives you a SecureString type.  I don't know if Xamarin is the same, but that's a way I figured to get a plain text string from the SecureString object.

Also there are some things that don't directly ask for Credential or have as a parameter but it is required.  The following does the trick for me in those cases

AuthenticationManager.Current.AddCredential(await GetWindowsCredential());

I have never had issue with this.  

As a general rule I will call into GetWindowsCredential anytime I need a credential unless there is a significant performance concern and I am sure that I am using the saved Credential over a short time.  But the token does expire and so if cached too long will no longer be valid

Hope that helps

Thanks,
-Joe
0 Kudos
AndyWright
Occasional Contributor

Hey Joe thanks for the reply.  I went ahead and tried the brute force approach and tried to generate a TokenCredential object from my user credentials right before generating an offline geodatabase from the feature service.  More or less doing it the same way you are, and I am currently getting a 401 error on that which is really strange.  We are now working with our network dude to see if something is wrong outside of our Portal implementation. 

0 Kudos
JoeHershman
MVP Regular Contributor

Have you tried to log in from the device directly to the sharing web site using Windows credentials?  

Thanks,
-Joe
0 Kudos
AndyWright
Occasional Contributor

I just tried that Joe and it worked fine.  Took my windows credentials and logged me in.

0 Kudos
JoeHershman
MVP Regular Contributor

Just thinking if it was a Portal issue you would also have a problem logging in directly, so scratching my head a bit on what would be wrong

Thanks,
-Joe
0 Kudos