Using secure services

Complexity: Intermediate Data Requirement: Installed with software Goal: Create a WPF application to consume a map service secured with web server or token-based authentication.

In this tutorial, you will create a WPF application in Microsoft Visual Studio 2008 to consume an ArcGIS Server map service secured with web server (e.g. IIS) or token-based authentication. Web server authentication methods include Integrated Windows authentication and HTTP Basic\Digest. Token-based authentication methods rely on a web service to authenticate a user and generate a token which is included in subsequent service requests to identify the user. A utility class is included with this tutorial to attach credential information to a web service proxy class, regardless of the authentication type. It is designed to be reused in your application.

Tutorial data

Sample code for this tutorial is available at: Security.zip

Create the WPF application project

Steps:
  1. In Visual Studio, click File > New Project.
  2. In the Add New Project dialog, under Project Types, click Visual C# Projects > Windows.
  3. Under Templates, select WPF Application. For the project name, specify SOAPSecurityWpf.
  4. Click OK. The project will open with a window named Window1.

Add references

Steps:
  1. Download the pre-generated proxy library and skip to the next step, or, add a web reference to a service catalog and secured map service. To add a web reference, navigate to Solution Explorer under the SOAPSecurityWpf project, right-click the References folder and select Add Service Reference. Navigate through the set of dialogs (shown below) to add a web reference. Add a reference to both the service catalog and map service. The references do not have to be on the server hosting a secure service, they will only be used to generate client-side proxy classes.
    • Example service catalog endpoint: http://serverapps.esri.com/arcgis/services?wsdl
    • Example map service endpoint: http://serverapps.esri.com/arcgis/services/California/MapServer?wsdl

    Add Web Reference dialog

  2. In Solution Explorer under the SOAPSecurityWpf project, right-click the References folder and select Add Reference. Add a reference to the System.Web library.

Add XAML and code

Steps:
  1. Open the XAML view of Window1.xaml. Add an Image and Button. Define a handler for the SizeChanged event on the Window. Use the following XAML as a guide:

    <Window x:Class="SOAPSecurityWpf.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Window1" Height="500" Width="600" SizeChanged="MyWindow_SizeChanged">
        <Grid>
            <Image x:Name="MyImage" />
            <Button x:Name="MyButton" Margin="10" Click="MyButton_Click" Width="100" Height="50" Content="Get Map"
                        HorizontalAlignment="Right" VerticalAlignment="Bottom" />
        </Grid>
    </Window>
    

  2. Open the code behind for the Window1 (Window1.xaml.cs). Add implementation code for the SizeChanged event to set to the size of the Image control to the size of the Window.

    private void MyWindow_SizeChanged(object sender, SizeChangedEventArgs e)
    
    {
    
        if (e.PreviousSize.Width == 0 && e.PreviousSize.Height == 0)
    
        {
    
         MyImage.Width = e.NewSize.Width;
    
         MyImage.Height = e.NewSize.Height;
    
        }
    }
    

  3. In the code behind Window1 (Window1.xaml.cs), add implementation code for the Click event on the Button. This will initiate a request to an ArcGIS Server map service to generate a dynamic map. You'll need to add a statement that references the namespace with the proxy classes (e.g. using ESRI.ArcGIS.SOAP;). The library which contains these classes was added in the first step of the Add References section. You'll notice a reference to a MapServiceProxy property in the code below. MapServiceProxy is a property on the Window1 class that contains the logic for creating/managing the SOAP Web proxy for the map service used in this example. The implementation of the property will contain the logic for authenticating access to the service.

    private void MyButton_Click(object sender, RoutedEventArgs rea)
    
    {
    
        // Initiate request to generate map image
    
        ShowSimpleMapImage();
    
    }
     
    
    private void ShowSimpleMapImage()
    
    {
    
        MapServerInfo mapServerInfo = MapServiceProxy.GetServerInfo(MapServiceProxy.GetDefaultMapName());
    
        MapDescription mapDescription = mapServerInfo.DefaultMapDescription;
     
    
        ImageType imageType = new ImageType();
    
        imageType.ImageFormat = esriImageFormat.esriImageJPG;
    
        imageType.ImageReturnType = esriImageReturnType.esriImageReturnURL;
     
    
        ImageDisplay imageDisplay = new ImageDisplay();
    
        imageDisplay.ImageHeight = (int)MyImage.Height;
    
        imageDisplay.ImageWidth = (int)MyImage.Width;
    
        imageDisplay.ImageDPI = 96;
     
    
        ImageDescription imageDescription = new ImageDescription();
    
        imageDescription.ImageDisplay = imageDisplay;
    
        imageDescription.ImageType = imageType;
     
    
        MapImage mapImage = MapServiceProxy.ExportMapImage(mapDescription, imageDescription);
    
        MyImage.Source = new BitmapImage(new Uri(mapImage.ImageURL, UriKind.Absolute));
    
    }
    

  4. In the code behind Window1 (Window1.xaml.cs), add implementation code to create\manage the MapServerProxy class associated with the secure map service. The MapServerProxy class resides in the ESRI.ArcGIS.SOAP.dll (see the first step of the Add References section). If you generated the proxy classes dynamically, this proxy class name will likely be different.

    Use a set of member variables to store proxy and authentication properties. The MapServiceProxy should return a SOAP web service proxy class for the map service endpoint you define (set the Url property). Before the proxy class instance is returned, use the utility class (AGSSOAPUtility.cs) included in the sample download code to add credential information to the proxy. A client timeout is maintained to match the token timeout. This way when the client timeout is reached, a new token will be generated. Otherwise you will need to explicitly check the validity of the token each time a proxy method is called - which means wrap all proxy method calls in a try\catch and check the exception to determine if it was caused by an expired token.

    private MapServerProxy _mapservice;
    
    private DateTime _endTime;
    
    private int _timeout = 1; //minutes - defines token service timeout
    
    // Token
    
    private string _serviceurl = "http://net931/arcgistoken/services/USA_Data/MapServer";
    
    private string _username = "test";
    
    private string _password = "test.test";
    
    private string _domain = "";
     
    
    // HTTP\Windows auth
    
    //private string _serviceurl = "http://net931/arcgis/services/USA_Data/MapServer";
    
    //private string _username = "user";
    
    //private string _password = "pass";
    
    //private string _domain = "net931";
     
    
    private MapServerProxy MapServiceProxy
    
    {
    
        get
    
        {
    
         if (_mapservice == null)
    
         {
    
           _mapservice = new MapServerProxy();
    
           _mapservice.Url = _serviceurl;
    
         }
    
    
         if (_mapservice.Credentials != null)
    
         {
    
           return _mapservice;
    
         }
    
         else if (DateTime.Now.CompareTo(_endTime) >= 0)
    
         {
    
           _endTime = DateTime.Now.AddMinutes(_timeout);
    
           _mapservice = AGSSOAPUtility.AuthenticateProxy.Authenticate
    
             (_mapservice, _username, _password, _domain, _timeout) as MapServerProxy;
    
         }  
    
         
         return _mapservice;
    
        }
    
    }
    

  5. Use the AGSSOAPUtility class included with the sample download to authenticate user credentials. The source code for the class is included below. In general, the workflow is as follows:
    1. Construct an web request to GET the WSDL for an ArcGIS Server SOAP web service.
    2. Try to get the response (which initiates the request). If an exception is thrown, check the error code. If unauthorized (401), assign credentials to the web request. If a token is required (499) or expired/invalid (498), generate a token and add it to the web request URL.
    3. If HTTP\Windows authentication, assign credential to the ArcGIS Server SOAP web proxy class. If token-based authentication, add the token to the URL for the ArcGIS Server SOAP web proxy class. Return the proxy class to the caller.
    NoteNote:

    The follow AGSSOAPUtility.cs assumes you are using the pre-generated proxy classes in the ESRI.ArcGIS.SOAP.dll.

    using System;
    
    using System.Collections.Generic;
    
    using System.Linq;
    
    using System.Text;
    
    using System.Net;
    
    using ESRI.ArcGIS.SOAP;
    
    using System.Collections.Specialized;
     
    
    namespace AGSSOAPUtility
    {
    
        class AuthenticateProxy
    
        {
    
            public static System.Web.Services.Protocols.SoapHttpClientProtocol
    
                Authenticate(System.Web.Services.Protocols.SoapHttpClientProtocol serviceproxy,
    
                string username, string password, string domain, int timeout)
    
            {
    
                string url_401 = serviceproxy.Url;
    
                if (serviceproxy.Url.Contains("?"))
    
                    url_401 = serviceproxy.Url.Substring(0, serviceproxy.Url.IndexOf("?"));                 
    
                url_401 += "?wsdl";
    
                HttpWebRequest webRequest_401 = null;
    
                webRequest_401 = (HttpWebRequest)HttpWebRequest.Create(url_401);
    
                webRequest_401.ContentType = "text/xml;charset=\"utf-8\"";
    
                webRequest_401.Method = "GET";
    
                webRequest_401.Accept = "text/xml";
    
                HttpWebResponse webResponse_401 = null;
    
                while (webResponse_401 == null || webResponse_401.StatusCode != HttpStatusCode.OK)
    
                {
    
                    try
    
                    {
    
                        webResponse_401 = (HttpWebResponse)webRequest_401.GetResponse();
    
                    }
    
                    catch (System.Net.WebException webex)
    
                    {
    
                        HttpWebResponse webexResponse = (HttpWebResponse)webex.Response;
    
                        if (webexResponse.StatusCode == HttpStatusCode.Unauthorized)
    
                        {
    
                            if (webRequest_401.Credentials == null)
    
                            {
    
                                webRequest_401 = (HttpWebRequest)HttpWebRequest.Create(url_401);
    
                                webRequest_401.ContentType = "text/xml;charset=\"utf-8\"";
    
                                webRequest_401.Method = "GET";
    
                                webRequest_401.Accept = "text/xml";
    
                                webRequest_401.Credentials = new NetworkCredential(username, password, domain);
    
                            }
    
                            else
    
                            {
    
                                // if original credentials not accepted, throw exception
    
                                throw webex;
    
                            }
    
                        }
    
                        // 499 - token required, 498 - invalid token
    
                        else if (webexResponse.StatusCode.ToString() == "499" ||
    
                            webexResponse.StatusCode.ToString() == "498")
    
                        {
    
                            string tokenServiceUrl = "";
    
                            ServiceCatalogProxy myCatalog = new ServiceCatalogProxy();
    
                            myCatalog.Url = serviceproxy.Url.Substring(0, serviceproxy.Url.IndexOf("/services") + 9);
     
    
                            if (myCatalog.RequiresTokens())
    
                                tokenServiceUrl = myCatalog.GetTokenServiceURL();
    
                            else
    
                                throw new Exception("Service does not require token but status code 499 returned");
    
                            if (string.IsNullOrEmpty(tokenServiceUrl))
    
                                throw new Exception("Token service url unavailable");
     
    
                            string url = tokenServiceUrl +           
                             string.Format("?request=getToken&username={0}&password={1}&timeout={2}",
                             username, password, timeout);
    
                            System.Net.HttpWebRequest request = (HttpWebRequest)System.Net.WebRequest.Create(url);
    
                            System.Net.WebResponse response = request.GetResponse();
    
                            System.IO.Stream responseStream = response.GetResponseStream();
    
                            System.IO.StreamReader readStream = new System.IO.StreamReader(responseStream);
     
    
                            string theToken = readStream.ReadToEnd();
    
                            webRequest_401 = (HttpWebRequest)HttpWebRequest.Create(url_401 + "&token=" + theToken);
    
                        }
    
                        else
    
                        {
    
                            // if status code unrecognized, throw exception   
    
                            throw webex;
    
                        }
    
                    }
    
                    catch (Exception ex) { throw ex; }
    
                }
    
                if (webResponse_401 != null)
    
                    webResponse_401.Close();
    
                if (webRequest_401.Credentials != null)
    
                    serviceproxy.Credentials = webRequest_401.Credentials;
    
                if (webRequest_401.RequestUri.ToString().Contains("token"))
    
                {
    
                    string myToken =
                       ParseStringIntoNameValueCollection(webRequest_401.RequestUri.ToString())["token"];
    
                    string baseServiceProxyUrl = serviceproxy.Url;               
    
                    if (serviceproxy.Url.Contains("?"))
    
                        baseServiceProxyUrl = serviceproxy.Url.Substring(0, serviceproxy.Url.IndexOf("?"));
    
                    
    
                    serviceproxy.Url = baseServiceProxyUrl + "?token=" + myToken;
    
                }
    
                return serviceproxy;
    
            }
     
    
            private static System.Collections.Specialized.NameValueCollection
    
                ParseStringIntoNameValueCollection(string argumentValues)
    
            {
    
                System.Collections.Specialized.NameValueCollection keyValColl =
    
                    new System.Collections.Specialized.NameValueCollection();
    
                string[] keyValuePairs = argumentValues.Split(new char[] { '&' },
    
                    StringSplitOptions.RemoveEmptyEntries);
    
                for (int i = 0; i < keyValuePairs.Length; i++)
    
                {
    
                    string keyval = keyValuePairs[i];
    
                    int index = keyval.IndexOf('=');
    
                    if (index >= 0)
    
                    {
    
                        string key = keyval.Substring(0, index);
    
                        string val = keyval.Substring(index + 1);
    
                        keyValColl.Add(key, val);
    
                    }
    
                }
    
                return keyValColl;
    
            }
    
        }
    }
    

  6. Run the application. Click on the Get Map button to request a new map image from the dynamic map service. The initial request will create the Map server proxy class and add authentication information. Click Get Map again to generate a new map image. The extent will not change and the map (image) is not interactive. This application is merely designed to illustrate the workflow for adding authentication details to a ArcGIS SOAP web proxy class.

    The sample application will match the simple interface described in this tutorial. It also includes a timer displayed in the lower left corner of the window to provide you with the seconds since the last request. This timer is useful in determining if a new token is successfully generated after the previous token expires. A screenshot of the sample application at runtime is provided below:

    Sample application displaying seconds since last token request

11/8/2016