Download the Source
Anyone who uses the Web is familiar with the two common forms of authentication.
The most common one is "form-based" authentication, in which you enter
some sort of user ID and password on a form. The Web application then authenticates
you based on some internal scheme, and typically uses a cookie to keep track
of the authentication status throughout the session. A less common method is
to use one of the authentication schemes internal to Internet Information Server
(IIS): Basic, Digest, Kerberos, or NTLM. You know this kind of authentication
is in use because your browser pops up a dialog box requesting a user name and
password before serving up the requested page. This type of authentication works
against the user list on the target server or domain. The common wisdom is that
Web services can't or shouldn't raise user interfaces for authentication purposes
(or any other), and thus most articles on Web services focus on how to get the
authentication information to the Web service, not how it gets to the client
in the first place.
Web services are used in many different ways. They allow applications to take
advantage of functionality published by Web sites. They allow implementation
of multi-tier applications. And in some architectures, the assumption that it
is not possible or advisable to prompt the user for a password simply isn't
true.
When we developed the Desaware Licensing System for .NET, we implemented the
activation server as an ASP.NET Web service. This has proven to be a good choice,
in that it is not only easy to use, but easy to deploy — especially on
shared hosts that are normally reluctant to host custom software (or charge
a premium to do so), but have no problem with custom ASP.NET software because
of its inherent stability and improved security model. The Licensing System
exposes two objects through a Web service: an activation object that is public
(in the sense of not requiring any authentication), and a management object
that is intended to be used to configure the licensing system (and thus requires
some form of authentication). ASP.NET is ideal for this type of architecture,
because IIS can set the authentication for each file independently.
The initial release of the Licensing System provided two types of authentication:
Windows integrated authentication for local systems or use with VPN access,
and IP-based authentication. The integrated authentication was originally limited
to using the default credentials of the license manager application (in other
words, the credentials of the user running the management program), but for
the latest release, we wanted to implement a model similar to the familiar browser
model where users are prompted for a user name password on those sites that
require it.
Disabling Anonymous Access to a Web Page or Web Service
The first step in turning on Windows authentication is to select the individual
Web service pages that you wish to secure (you can, of course, secure an entire
Web site in this manner), as shown in FIGURE 1.

FIGURE 1: Selecting the page(s) to secure
When you bring up the properties for the selected page, you can choose the
File Security tab and edit the Anonymous access and authentication control for
the page, as shown in FIGURE 2.

FIGURE 2: Editing the File Security settings
Turn off Anonymous access for the page, and select the type of authentication
you wish to use, as shown in FIGURE 3. It's best to avoid Basic authentication
unless you're accessing the Web service via SSL (in which case the channel encryption
will protect the information).

FIGURE 3: The Authentication Methods dialog box
In this case integrated Windows authentication is a fairly safe choice because
the client application is itself a .NET application, and thus running on Windows
with full access to the .NET Framework. In most cases you'll use NTLM, which
is fairly secure even on a regular http channel, but you can also use Kerberos.
Both systems allow authentication using a password without actually sending
the password across the connection. Instead, they transfer information that
is encrypted with the password, a process that will only work if both the client
and server agree on the password.
Assigning Credentials to a Web Service Proxy
When you add a Web reference to a .NET application, Visual Studio builds a
hidden Web proxy class for the Web service. The Web proxy class is in the file
Reference.vb in the Web References subdirectory corresponding to the service.
This class, which inherits from the System.Web.Services.Protocols.SoapHttpClientProtocol
class, provides a strongly typed object with members that correspond to those
of the Web service. For example, if the Web service has a method named "Test",
the proxy class will have a method named Test, which when called invokes the
Web service method using the proxy class Invoke method.
In this example, the secured Web service is called Service2ws.Service2, and
it has one function, "Test" that retrieves a string. You create the
proxy object as you would any other object — using the New statement:
| Dim mywsproxy As New Service2ws.Service2 |
The proxy object by default has a URL for the Web service, and a Credentials
property that contains a set of credentials that will be sent with the request.
You can create a new credential and assign it to the Credentials
property of the proxy as follows:
Dim mycredentials As New CredentialCache
Dim m_cred As NetworkCredential
m_cred = New NetworkCredential(txtUser.Text, _
txtPassword.Text,
txtDomain.Text)
mycredentials.Add(New Uri(mywsproxy.Url), _
credform.AuthenticationType, m_cred)
mywsproxy.Credentials = mycredentials |
Simple enough, but all that demonstrates is how to set up the authentication
for the Web service call. Let's take a step back and look at the entire scenario.
The Client Scenario
Before implementing the client security, you need to put some thought into
what kinds of authentication the service will support. In the case of our licensing
system, we wanted to offer as much flexibility as possible. Some people might
secure the service using IP filters — not a bad practice in cases where
you have a dedicated IP range for in-house systems. Others might want to authenticate
against the user who is running the client — a good choice where clients
have VPN access into the internal network, or must be authenticated against
a domain to access the service. In both these cases we didn't want to request
a user ID and password — the client was already authorized. For this reason,
we first try to access the Web service directly, only using the default credentials
of the user. You can set a Web proxy to use the current user credentials by
setting the Credentials property as follows:
| m_Service.Credentials =
CredentialCache.DefaultCredentials |
The sample code for this article doesn’t do this because it is designed to
run on the same system as the Web service, and if the default credentials were
set the client would always authenticate, so you wouldn’t have a chance to see
the full sequence.
The NetworkCredential object is obtained using a form
that has text boxes for the user name, domain, and password. It also has a combo
box that allows you to select the type of credential to create. The authentication
type is one of the following strings: Basic, Digest, Kerberos, or NTLM (case
matters):
| Private m_cred As NetworkCredential
Public ReadOnly Property NewCredentials() As NetworkCredential
Get
Return m_cred
End Get
End Property
Public ReadOnly Property AuthenticationType() As String
Get
Return cmbAuthentication.Text
End Get
End Property
Private Sub cmdCancel_Click(ByVal sender As System.Object, _
ByVal e As
System.EventArgs) Handles cmdCancel.Click
m_cred = Nothing
Me.Close()
End Sub
Private Sub cmdOk_Click(ByVal sender As System.Object, _
ByVal e As
System.EventArgs) Handles cmdOk.Click
Try
m_cred = New
NetworkCredential(txtUser.Text, _
txtPassword.Text, txtDomain.Text)
Me.Close()
Catch ex As Exception
MsgBox(ex.Message)
End Try
End Sub
Private Sub frmcredentials_Load(ByVal sender As System.Object, _
ByVal
e As System.EventArgs) Handles MyBase.Load
cmbAuthentication.SelectedIndex = 0
End Sub |
The actual sequence for invoking the method is as follows:
- Try to access the method using the default credentials.
- If a WebException occurs, check to see if it is
a protocol error that contains a 401 (unauthorized access) error message.
- If so, bring up the credentials form and prompt the user for the required
information.
- Retry the operation using the new credentials.
The actual code is as follows:
Private Sub cmdTest2_Click(ByVal sender As System.Object, _
ByVal e As
System.EventArgs) Handles cmdTest2.Click
ListBox1.Items.Clear()
Dim mywsproxy As New Service2ws.Service2
Try
ListBox1.Items.Add(mywsproxy.Test())
Catch webex As WebException
ListBox1.Items.Add("Exception:
" & webex.Message)
If webex.Status =
Net.WebExceptionStatus.ProtocolError _
AndAlso
webex.Message.IndexOf("401") > 0 Then
Dim mycredentials As
New CredentialCache
Dim credform As New
frmcredentials
ListBox1.Items.Add("Prompting user for credentials")
credform.ShowDialog()
If
credform.NewCredentials Is Nothing Then
ListBox1.Items.Add("This service requires authentication")
Exit
Sub
End If
mycredentials.Add(New
Uri(mywsproxy.Url), _
credform.AuthenticationType,
credform.NewCredentials)
mywsproxy.Credentials =
mycredentials
Try
ListBox1.Items.Add(mywsproxy.Test())
Catch ex As Exception
ListBox1.Items.Add(ex.Message)
End Try
End If
Catch ex As Exception
ListBox1.Items.Add("Exception:
" & ex.Message)
End Try
End Sub
|
Obviously, this is a simple example. In a real application that makes ongoing
use of the Web service, you would continue to reuse the proxy (and the credentials),
so you wouldn't prompt the user for information on each Web request.
Conclusion
This example demonstrates one answer to a critical (and often unasked) question:
Where does the authentication information live when it's not being used? If
you stop with the assumption that Web services should not prompt the user for
this information, there's an implication that authentication information must
be stored somewhere, perhaps on the client machine. This is a workable solution,
if the machine itself is secure. But what if it's a laptop? Considering the
rate of laptop theft, keeping this information on the machine is questionable
(to say the least). In cases where you are implementing a client-server architecture,
where the client application is closely associated with the Web service (as
compared to a Web service designed for public or general developer consumption),
prompting the user for the information might be exactly what you want to do.