T U T O R I A L S
In this tutorial, we will create an 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.
Sample code for this tutorial is available here: Security.zip
Create the WPF Application project
In Visual Studio, click File, New Project.
In the Add New Project dialog, under Project Types, click Visual C# Projects > Windows.
Under Templates, select WPF Application. For the project name, specify SOAPSecurityWpf.
Click OK. The project will open with a window named Window1.
Add references
Download the pre-generate 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,
in Solution Explorer under the SOAPSecurityWpf project, right-click the
References folder and select Add Service Reference. Navigate through a
set of dialogs (shown below) to add a Web reference. Add
a reference to both a service catalog and map service. Note,
they do not have to be on the server hosting a secure service, these references
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
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
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>
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;
}
}In the code behind for the Window1 (Window1.xaml.cs),
add implementation code for the Click event on the Button. This
will initiate a request to an ArcGIS Server Web map service to generate
a dynamic map. You'll need to add a using statement
that references the the namespace with the proxy classes (e.g. using ESRI.ArcGIS.SOAP;).
The library which contains these classes was added
in the Add References section, step 1. 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));
}In the code behind for the Window1 (Window1.xaml.cs),
add implementation code to create\manage the MapServerProxy class associated
with the secure map service. Note the MapServerProxy
class resides in the ESRI.ArcGIS.SOAP.dll (see Add References, step 1).
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;
}
}
Use the AGSSOAPUtility class included
with the sample download to authenticate the user credentials. The
source code for the class is included below. In
general, the workflow is as follows:
a. Construct an Web request to GET the WSDL for an ArcGIS Server SOAP
Web service.
b. 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.
c. 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.
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;
}
}
}