Select to view content in your preferred language

Help with OAuth

2710
30
Jump to solution
10-04-2023 11:30 AM
BrianBulla1
Occasional Contributor

Hi,

I'm just starting with the Maps SDK today, so sorry in advance for the stupid questions.  🙂

So I'm trying to do something that I think should be super simple, but I cannot for the life of me get it to work.  I'm using the DisplayAMap sample, changing the portal item to one under my AGOL account, and trying to display it.  The missing componenet is the OAuth, since I'll need to enter a username/password to get to the webmap.

I'm looking at the "Secutrity" samples in the .NET WPF app, but don't quite understand how to integrate it into what I already have working in the DisplayAMap Sample.

There seems to be this "ArcGISLoginPrompt.cs" file, that doesn't display the code for in the app.  I'm guessing that's an important part of it??

Sorry for the cryptic question....I just have no clue what to even ask.

0 Kudos
1 Solution

Accepted Solutions
ThadTilton
Esri Contributor

Hi Brian,

I was able to tinker with this a bit today. I think I ran into the same problem you described and I was able to find a solution.

For me, I found that having an API key set prevented the app from challenging for the secured web map. With the API key defined (in App.OnStartup), the app would throw an exception that said I don't have permission to access the resource (without challenging me for a login). If I commented out the line that set the API key, I was challenged for username/password and the secure webmap loaded as expected. It seems that the app was trying to authenticate with the API key and didn't try to use OAuth at that point.

I think this works with the tutorial data because it's authenticating for a single (premium) layer rather than for a secure (private) web map item. In this case, it seems to effectively use the API key for the basemap layer and OAuth for the traffic layer. I think it's a little unclear how the API key and user authentication work together. I think this needs some better documentation.

Looking at the developer guide, I found a description of setting an API key's scope to allow access to private ArcGIS Online items. This is currently in beta and has some limitations. The API key tutorial also describes the steps to enable that access.

You might want to try following the instructions to scope your API key to access your web map and see if you can access it without using OAuth. Another option is to try just commenting out the line that sets the API key (in your project based on the tutorial) and see if OAuth challenges the user for access.

-Thad

View solution in original post

30 Replies
JoeHershman
MVP Regular Contributor

A little more detail about what you issues are, platform.  The security sample in github are hard to follow because they use shared code, better to follow in the sample explanation on the site.

Thanks,
-Joe
BrianBulla1
Occasional Contributor

Hi @JoeHershman .

So basically what I have done is trying to combine the Display A Map tutorial and the Oath 2.0 tutorial found here (https://developers.arcgis.com/net/tutorials/) and get it to work with a secured webmap in my own AGOL account.

I can get it to build with no errors, but nothing is showing up in the WPF form.  It's just a blank map.

Not quite sure how to troubleshoot it.

0 Kudos
BrianBulla1
Occasional Contributor

It seems to be stalling at this line, where the web map is getting created.  It never gets to the point where it asks for the username/password for AGOL.

BrianBulla1_0-1696959154806.png

 

0 Kudos
JoeHershman
MVP Regular Contributor

Hard to know what you have gotten done.

  1. Have you setup the OAuth app inside of ArcGIS Online?  This is discussed in the sample Authenticate with OAuth 
    1. You can do through dev account if you are using AGOL tied to dev account Register an OAuth application 
    2. Do on AGOL, this is a bit old so there are slight GUI changes How to Implement OAuth 2.0 for ArcGIS 
  2. You need to implement the IOAuthAuthorizeHandler.  See at bottom
  3. Wire up everything.  This is done before you can make a call that will open the OAuth window.  AppClientID, ClientSecret, RedirectUrl all come from the OAuth app in AGOL (or dev center)
private void Initialize(Uri serverUri)
{
	try
	{
		var redirectUri = new Uri(RedirectUrl);
		ServerInfo serverInfo = new ServerInfo(serverUri)
		{
			TokenAuthenticationType = TokenAuthenticationType.OAuthAuthorizationCode,
			OAuthClientInfo = new OAuthClientInfo(AppClientId, redirectUri)
			{
				UserInterfaceStyle = UserInterfaceStyle.Dark,
				ClientSecret = ClientSecret,
			},

		};

		AuthenticationManager.Current.RegisterServer(serverInfo);
		AuthenticationManager.Current.OAuthAuthorizeHandler = _oAuthAuthorizeHandler;
		AuthenticationManager.Current.ChallengeHandler = new ChallengeHandler(CreateCredentialAsync);
	}
	catch (Exception e)
	{
		_log?.Log(e.Message, Category.Exception, Priority.None);
	}
}

private async Task<Credential> CreateCredentialAsync(CredentialRequestInfo info)
{
	try
	{
		Credential credential = null;

		try
		{
			if (info.ServiceUri != null)
			{
				var options = new GenerateTokenOptions { TokenExpirationInterval = TimeSpan.FromDays(1) };
				credential = await AuthenticationManager.Current.GenerateCredentialAsync(info.ServiceUri, options);
				
				AuthenticationManager.Current.AddCredential(credential);

				ConnectionStatus = ConnectionStatus.Connected;
			}
		}
		catch (OperationCanceledException)
		{
			ConnectionStatus = ConnectionStatus.NotConnected;
			// Login was cancelled, no need to display an error to the user.
		}
		catch (Exception e)
		{
			_log.Log(e.Message, Category.Warn, Priority.None);
		}

		return credential;

	}
	catch (Exception)
	{
		return null;
	}
}

 

One annoying with the API is that connecting to Portal does not automatically trigger the the challenge handler.  So you have to have the code in the method that connects to portal that, imo, should only be in ChallengeHandler

public async Task<ArcGISPortal> OpenPortalAsync(string portalUrl, bool refresh = false)
{
	var sharingUri = GetSharingUri(portalUrl);
        //THIS LINE SHOULD OPEN WINDOW!
	var credential = await AuthenticationManager.Current.GenerateCredentialAsync(sharingUri);

	AuthenticationManager.Current.AddCredential(credential);

	_portal = await ArcGISPortal.CreateAsync(sharingUri);
	PortalUser = _portal.User;

	return _portal;
}

private Uri GetSharingUri(string portalUrl)
{
	return new Uri($"{portalUrl}/sharing/rest");
}

 

 

 

IOAuthAuthorizeHandler implementation from esri sample:

// In a desktop (WPF) app, an IOAuthAuthorizeHandler component is used to handle some of the OAuth details. Specifically, it
//     implements AuthorizeAsync to show the login UI (generated by the server that hosts secure content) in a web control.
//     When the user logs in successfully, cancels the login, or closes the window without continuing, the IOAuthAuthorizeHandler
//     is responsible for obtaining the authorization from the server or raising an OperationCanceledException.
public class OAuthAuthorizeHandler : IOAuthAuthorizeHandler
{
	// Window to contain the OAuth UI.
	private Window _authWindow;

	// Use a TaskCompletionSource to track the completion of the authorization.
	private TaskCompletionSource<IDictionary<string, string>> _tcs;

	// URL for the authorization callback result (the redirect URI configured for your application).
	private string _callbackUrl;

	// URL that handles the OAuth request.
	private string _authorizeUrl;

	// Function to handle authorization requests, takes the URIs for the secured service, the authorization endpoint, and the redirect URI.
	public Task<IDictionary<string, string>> AuthorizeAsync(Uri serviceUri, Uri authorizeUri, Uri callbackUri)
	{
		if (_tcs != null && !_tcs.Task.IsCompleted)
			throw new Exception("Task in progress");

		_tcs = new TaskCompletionSource<IDictionary<string, string>>();

		// Store the authorization and redirect URLs.
		_authorizeUrl = authorizeUri.AbsoluteUri;
		_callbackUrl = callbackUri.AbsoluteUri;

		// Call a function to show the login controls, make sure it runs on the UI thread for this app.
		Dispatcher dispatcher = Application.Current.Dispatcher;
		if (dispatcher == null || dispatcher.CheckAccess())
		{
			AuthorizeOnUIThread(_authorizeUrl);
		}
		else
		{
			Action authorizeOnUIAction = () => AuthorizeOnUIThread(_authorizeUrl);
			dispatcher.BeginInvoke(authorizeOnUIAction);
		}

		// Return the task associated with the TaskCompletionSource.
		return _tcs.Task;
	}

	// Challenge for OAuth credentials on the UI thread.
	private void AuthorizeOnUIThread(string authorizeUri)
	{
		// Create a WebBrowser control to display the authorize page.
		WebBrowser webBrowser = new WebBrowser();

		// Handle the navigation event for the browser to check for a response to the redirect URL.
		webBrowser.Navigating += WebBrowserOnNavigating;

		// Display the web browser in a new window.
		_authWindow = new Window
		{
			Content = webBrowser,
			Width = 450,
			Height = 450,
			WindowStartupLocation = WindowStartupLocation.CenterOwner
		};

		// Set the app's window as the owner of the browser window (if main window closes, so will the browser).
		if (Application.Current != null && Application.Current.MainWindow != null)
		{
			_authWindow.Owner = Application.Current.MainWindow;
		}

		// Handle the window closed event then navigate to the authorize url.
		_authWindow.Closed += OnWindowClosed;
		webBrowser.Navigate(authorizeUri);

		// Display the window.
		_authWindow.ShowDialog();
	}

	// Handle the browser window closing.
	private void OnWindowClosed(object sender, EventArgs e)
	{
		// If the browser window closes, return the focus to the main window.
		if (_authWindow != null && _authWindow.Owner != null)
		{
			_authWindow.Owner.Focus();
		}

		// If the task wasn't completed, the user must have closed the window without logging in.
		if (!_tcs.Task.IsCompleted)
		{
			// Set the task completion source exception to indicate a canceled operation.
			_tcs.SetCanceled();
		}

		_authWindow = null;
	}

	// Handle browser navigation (content changing).
	private void WebBrowserOnNavigating(object sender, NavigatingCancelEventArgs e)
	{
		// Check for a response to the callback url.
		const string portalApprovalMarker = "/oauth2/approval";
		WebBrowser webBrowser = sender as WebBrowser;

		Uri uri = e.Uri;

		// If no browser, uri, or an empty url, return.
		if (webBrowser == null || uri == null || string.IsNullOrEmpty(uri.AbsoluteUri))
			return;

		// Check for redirect.
		bool isRedirected = uri.AbsoluteUri.StartsWith(_callbackUrl) ||
			_callbackUrl.Contains(portalApprovalMarker) && uri.AbsoluteUri.Contains(portalApprovalMarker);

		// Check if browser was redirected to the callback URL. (This indicates succesful authentication.)
		if (isRedirected)
		{
			e.Cancel = true;

			// Call a helper function to decode the response parameters.
			IDictionary<string, string> authResponse = DecodeParameters(uri);

			// Set the result for the task completion source.
			_tcs.SetResult(authResponse);

			// Close the window.
			if (_authWindow != null)
			{
				_authWindow.Close();
			}
		}
	}

	private static IDictionary<string, string> DecodeParameters(Uri uri)
	{
		// Create a dictionary of key value pairs returned in an OAuth authorization response URI query string.
		string answer = "";

		// Get the values from the URI fragment or query string.
		if (!string.IsNullOrEmpty(uri.Fragment))
		{
			answer = uri.Fragment.Substring(1);
		}
		else
		{
			if (!string.IsNullOrEmpty(uri.Query))
			{
				answer = uri.Query.Substring(1);
			}
		}

		// Parse parameters into key / value pairs.
		Dictionary<string, string> keyValueDictionary = new Dictionary<string, string>();
		string[] keysAndValues = answer.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries);
		foreach (string kvString in keysAndValues)
		{
			string[] pair = kvString.Split('=');
			string key = pair[0];
			string value = string.Empty;
			if (key.Length > 1)
			{
				value = Uri.UnescapeDataString(pair[1]);
			}

			keyValueDictionary.Add(key, value);
		}

		// Return the dictionary of string keys/values.
		return keyValueDictionary;
	}
}

 

Thanks,
-Joe
0 Kudos
BrianBulla1
Occasional Contributor

Thanks @JoeHershman .

If I use that Authenticate with OAuth sample and modify the code, I can get it to work with my web map I am testing with, so I must have the OAuth setup properly in my Developers Account.

I'll have to figure out how to add what is in this OAuth sample into my own app.  It seems quite different than the sample I have been using here:  https://developers.arcgis.com/net/security-and-authentication/tutorials/access-services-with-oauth-2...

 

0 Kudos
BrianBulla1
Occasional Contributor

OK....so I've gotten things to the point where I think they should be working, but when I use the webmap ID for one in my organization, it just doesn't work.  It just sort of hangs on the loading of the map and asking for login credentials.

If I sub my webmap ID for the ID of the public ones that is in the esri samples, I get prompted for the login credentials and then everything loads up.

Is there maybe a setting on my webmap I need to adjust??

I have no clue why I can't get this to work.  Here is some code if it helps.  Line #64 is where I am seeing the hang.

 

//   Copyright 2022 Esri
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//   https://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

using System;
using System.Collections.Generic;
using System.Text;
using Esri.ArcGISRuntime.Geometry;
using Esri.ArcGISRuntime.Mapping;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Esri.ArcGISRuntime.Portal;

namespace AccessServicesWithOAuth
{

    class MapViewModel : INotifyPropertyChanged
    {
        public MapViewModel()
        {
            //SetupMap();
            _ = SetupMap();
        }

        public event PropertyChangedEventHandler? PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private Map? _map;
        public Map? Map
        {
            get { return _map; }
            set
            {
                _map = value;
                OnPropertyChanged();
            }
        }

        private async Task SetupMap()
        {

            // Add the ArcGIS Online URL to the authentication helper.
            AuthenticationHelper.RegisterSecureServer("https://www.arcgis.com/sharing/rest");

            // Create a portal. If a URI is not specified, www.arcgis.com is used by default.
            ArcGISPortal portal = await ArcGISPortal.CreateAsync();

            // Get the portal item for a web map using its unique item id.
            PortalItem mapItem = await PortalItem.CreateAsync(portal, "41281c51f9de45edaf1c8ed44bb10e30");  //This is the esri sample ID
            
            //PortalItem mapItem = await PortalItem.CreateAsync(portal, "29ad2fc9b8794fb0b6dc1ca02948613d");    //This is the ID of my webmap.  If I change the sharing on this to 
                                                                                                              //"Everyone", then it loads up ok.  Only won't load up when you need 
                                                                                                              //to enter credentials.

            // Create the map from the item.
            Map map = new Map(mapItem);

            // Create a layer to display the ArcGIS World Traffic service.
            var trafficServiceUrl = "https://traffic.arcgis.com/arcgis/rest/services/World/Traffic/MapServer";
            var trafficLayer = new ArcGISMapImageLayer(new Uri(trafficServiceUrl));

            // Handle changes in the traffic layer's load status to check for errors.
            trafficLayer.LoadStatusChanged += TrafficLayer_LoadStatusChanged;

            // Add the traffic layer to the map's data layer collection.
            map.OperationalLayers.Add(trafficLayer);

            // To display the map, set the MapViewModel.Map property, which is bound to the map view.
            this.Map = map;

        }


        private void TrafficLayer_LoadStatusChanged(object? sender, Esri.ArcGISRuntime.LoadStatusEventArgs e)
        {
            // Report the error message if the traffic layer fails to load.
            if (e.Status == Esri.ArcGISRuntime.LoadStatus.FailedToLoad)
            {
                var trafficLayer = sender as ArcGISMapImageLayer;
                System.Windows.MessageBox.Show(trafficLayer?.LoadError?.Message, "Traffic layer did not load");
            }
        }

    }
}

 

0 Kudos
JoeHershman
MVP Regular Contributor

Are you passing an API Key?  You need to pass in if using a public map.  You can do when loading map or in the Runtime Initialize 

Thanks,
-Joe
0 Kudos
BrianBulla1
Occasional Contributor

Yes, I am passing my API key.  Do I need to do something in my Dev Account to link this API key directly to the webmap .... or some other kind of setting??

BrianBulla1_0-1697474100085.png

 

0 Kudos
JoeHershman
MVP Regular Contributor

You need to call ArcGISRuntime.Initialize after setting the API Key

Thanks,
-Joe
0 Kudos