I’ve been writing some ASP.NET pages for my Exchange boxes at the moment and although there is a lot of good information out there about ASP.Net finding good information about using ASP.Net with Exchange can be a little challenging so I though I share some info and links that I found useful. For this post I’m looking at using NTLM authentication on a remote web server to access Exchange 200x box on another server (if your using Form Based Authentication have a look at this )
Authentication
This was one of the hardest things for me to get my head around from classic asp. With “ASP.NET it provides an out-of-process execution model, which protects the server process from user code” this provides a good primer on the ASP.NET process model
The import part of this is what account the ASP.NET process runs under by default in IIS5 it runs under a local SAM account “ASPNET”. On an IIS6 box (win 2003) the ASP.NET process runs under the default Network Service (system account) account. This becomes important when you start to try and delegate authentication to access another server.
Impersonation
Impersonation is important because it allows you to access a mailbox in your ASP.Net page using the credentials of the user you have authenticated on your webserver. The important thing to remember about impersonation is that by itself it only allows you to access resources on the local server using the credentials you’re impersonating. There is a good KB article on impersonation that gives you the different methods you can use to perform impersonation from using the web.config file to doing it in-code. I like the in-code method to me its a more flexible and secure way to do it.
Delegation
“Impersonation enables ASP.NET to execute code and access resources in the context of an authenticated and authorized user, but only on the server where ASP.NET is running. To access resources located on another computer on behalf of an impersonated user requires authentication delegation (or delegation for short).” Ref this
This is pretty important thing if you want your remote web server to be able to access your mailbox. Another import thing from the “Integrated Windows Authentication” section is “When used in conjunction with Kerberos v5 authentication, IIS can delegate security credentials among computers running Windows 2000 or later that are trusted and configured for delegation.”. Which means to get delegation to work you need to be using Kerberos and you need to go off and read another couple of articles which details how to set this up this msdn article and Q810572. The import part in these articles is if you’re running IIS5 in the default config “the account used to run the server process (the process that performs impersonation) is allowed to delegate client accounts. You must configure the user account under which the server process runs, or if the process runs under the local SYSTEM account.”. On a IIS5 box the default user is the aspnet user which means you will need to change the account being used to one that can be trusted for delegation (eg the system account).
(Note: this was one thing that I found really hard to get good accurate information on what I’ve included in this post is what I found worked for me. Running the asp.net process under the system account does open up certain security issues but hey so does enabling delegation on your web server if someone wishes’s to correct me please send me an email. If you’re using 2003 this conversation is kind of redundant plus you have the ability to do constrained delegation)
SSL and Self signed certificates
Running WebDAV over http remotely is not a great idea because your going to be shipping your data as clear text over the network So its best to use https to do all your conversations with the Exchange server. If you’re down the less financially able spectrum you may not be able to afford to be using registered SSL certificates. In this case your application may have to deal with certificate warning messages. You can do this “by implementing your own CertificatePolicy class (which implements the ICertificatePolicy interface). In this class you will have to write your own CheckValidationResult function that has to return true or false” have a read of http://weblogs.asp.net/jan/archive/2003/12/04/41154.aspx which gives a good description and some code to work around this.
Some Examples
I’ve come up with a couple of samples that demonstrate all the stuff I’ve talked about. Both samples do the same thing they use the System.Directroyservices namespace to build a URL to access the user’s logon information (based on the user that is authenticated on the IIS server). Once the URL is built it then connects to the mailbox and queries all the contacts in a user’s mailbox. The first example uses a XSL file to do a transform of the XML returned from the WebDAV query. They both use SSL and implement a method to override any certificate popup warnings. The second example loads the result of the WebDAV query into an ADO.NET dataset and then displays the result in a datagrid. Both code samples need to be located in a directory that has all other authentication except windows authentication turned off. I’ve posted a downloadable copy of both examples here the second example looks like.
<%@ Page %>
<%@Import namespace="System.Data"%>
<%@Import namespace="System.Net"%>
<%@Assembly name="System.DirectoryServices, Version=1.0.5000.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a, Custom=null"%>
<%@Import namespace="System.DirectoryServices"%>
<%@Import namespace="System.Security.Cryptography.X509Certificates"%>
<script language="vb" runat="server">
Public Class AcceptAllCertificatePolicy
Implements ICertificatePolicy
Public Overridable Function CheckValidationResult(ByVal srvPoint As ServicePoint,
ByVal certificate As X509Certificate, ByVal request As WebRequest, ByVal problem
As Integer) As Boolean Implements ICertificatePolicy.CheckValidationResult
Return True 'this accepts all certificates
End Function
End Class
Sub Page_Load(sender As Object, e As EventArgs)
System.Net.ServicePointManager.CertificatePolicy = New
AcceptAllCertificatePolicy
Dim Request As System.Net.HttpWebRequest
Dim Response As System.Net.HttpWebResponse
Dim strRootURI As String
Dim strQuery As String
Dim bytes() As Byte
Dim workrow As System.Data.DataRow
Dim resrow As System.Data.DataRow
Dim impersonationContext As
System.Security.Principal.WindowsImpersonationContext
Dim currentWindowsIdentity As System.Security.Principal.WindowsIdentity
currentWindowsIdentity = CType(User.Identity,
System.Security.Principal.WindowsIdentity)
impersonationContext = currentWindowsIdentity.Impersonate()
Dim MyCredentialCache As System.Net.CredentialCache
Dim resdataset As New System.Data.DataSet
Dim RequestStream As System.IO.Stream
Dim ResponseStream As System.IO.Stream
Dim ResponseXmlDoc As System.Xml.XmlDocument
Dim DisplayNameNodes As System.Xml.XmlNodeList
Dim objsearch As New System.DirectoryServices.DirectorySearcher
Dim strrootdse As String = objsearch.SearchRoot.Path
Dim objdirentry As New system.DirectoryServices.DirectoryEntry(strrootdse)
Dim objresult As system.DirectoryServices.SearchResult
Dim stremailaddress As String
Dim strhomeserver As String
objsearch.Filter = "(&(&(&(& (mailnickname=*) (|
(&(objectCategory=person)(objectClass=user)(|(homeMDB=*)" _
& "(msExchHomeServerName=*)))
)))(objectCategory=user)(userPrincipalName=*)(mailNickname=" &
System.Environment.UserName & ")))"
objsearch.SearchScope = DirectoryServices.SearchScope.Subtree
objsearch.PropertiesToLoad.Add("mail")
objsearch.PropertiesToLoad.Add("msExchHomeServerName")
objsearch.Sort.Direction = DirectoryServices.SortDirection.Ascending
objsearch.Sort.PropertyName = "mail"
Dim colresults As DirectoryServices.SearchResultCollection = objsearch.FindAll()
For Each objresult In colresults
stremailaddress = objresult.GetDirectoryEntry().Properties("mail").Value
strhomeserver =
objresult.GetDirectoryEntry().Properties("msExchHomeServerName").Value
Next
Dim emailNameNodes As System.Xml.XmlNodeList
strhomeserver = Right(strhomeserver, Len(strhomeserver) - (InStr(strhomeserver,
"cn=Servers/cn=") + 13))
strRootURI = "https://" & strhomeserver & "/exchange/" & stremailaddress &
"/contacts/"
strQuery = "<?xml version=""1.0""?>" & _
"<D:searchrequest xmlns:D = ""DAV:"" >" & _
"<D:sql>SELECT ""urn:schemas:contacts:cn"",
""http://schemas.microsoft.com/mapi/email1emailaddress"" " & _
"FROM """ & strRootURI & """" & _
"WHERE ""DAV:ishidden"" = false AND ""DAV:isfolder"" = false AND
""DAV:contentclass"" = 'urn:content-classes:person'" & _
"</D:sql></D:searchrequest>"
Request = CType(System.Net.WebRequest.Create(strRootURI), _
System.Net.HttpWebRequest)
Request.Credentials = System.Net.CredentialCache.DefaultCredentials
Request.Method = "SEARCH"
bytes = System.Text.Encoding.UTF8.GetBytes(strQuery)
Request.ContentLength = bytes.Length
RequestStream = Request.GetRequestStream()
RequestStream.Write(bytes, 0, bytes.Length)
RequestStream.Close()
Request.ContentType = "text/xml"
Request.Headers.Add("Translate", "F")
Response = CType(Request.GetResponse(), System.Net.HttpWebResponse)
ResponseStream = Response.GetResponseStream()
ResponseXmlDoc = New System.Xml.XmlDocument
ResponseXmlDoc.Load(ResponseStream)
DisplayNameNodes = ResponseXmlDoc.GetElementsByTagName("d:cn")
emailNameNodes = ResponseXmlDoc.GetElementsByTagName("e:email1emailaddress")
Dim resultstable As DataTable
resultstable=new DataTable()
resultstable.Columns.Add("Contact Name")
resultstable.Columns.Add("Email Address")
AddRow(resultstable, "glenscales@yahoo.com", "Tests")
If DisplayNameNodes.Count > 0 Then
Dim i As Integer
For i = 0 To DisplayNameNodes.Count - 1
AddRow(resultstable, emailNameNodes(i).InnerText, DisplayNameNodes(i).InnerText)
Next
Else
Console.WriteLine("No non-folder items found...")
End If
Showcontacts.DataSource=resultstable
Showcontacts.DataBind()
impersonationContext.Undo()
End Sub
Sub AddRow(resultstable As DataTable, Emailaddress As String, Name As String)
Dim row As DataRow
row=resultstable.NewRow()
row("Contact Name")=Name
row("Email Address")=Emailaddress
resultstable.Rows.Add(row)
End Sub
</script>
<HTML>
<body>
<form id="Recipe1416vb" method="post" runat="server">
<asp:DataGrid ID="Showcontacts" Runat="server" AutoGenerateColumns="False">
<Columns>
<asp:BoundColumn DataField="Contact Name" HeaderText="Contact Name" />
<asp:BoundColumn DataField="Email Address" HeaderText="Email Address" />
</Columns>
</asp:DataGrid>
</form>
</body>
</HTML>
Authentication
This was one of the hardest things for me to get my head around from classic asp. With “ASP.NET it provides an out-of-process execution model, which protects the server process from user code” this provides a good primer on the ASP.NET process model
The import part of this is what account the ASP.NET process runs under by default in IIS5 it runs under a local SAM account “ASPNET”. On an IIS6 box (win 2003) the ASP.NET process runs under the default Network Service (system account) account. This becomes important when you start to try and delegate authentication to access another server.
Impersonation
Impersonation is important because it allows you to access a mailbox in your ASP.Net page using the credentials of the user you have authenticated on your webserver. The important thing to remember about impersonation is that by itself it only allows you to access resources on the local server using the credentials you’re impersonating. There is a good KB article on impersonation that gives you the different methods you can use to perform impersonation from using the web.config file to doing it in-code. I like the in-code method to me its a more flexible and secure way to do it.
Delegation
“Impersonation enables ASP.NET to execute code and access resources in the context of an authenticated and authorized user, but only on the server where ASP.NET is running. To access resources located on another computer on behalf of an impersonated user requires authentication delegation (or delegation for short).” Ref this
This is pretty important thing if you want your remote web server to be able to access your mailbox. Another import thing from the “Integrated Windows Authentication” section is “When used in conjunction with Kerberos v5 authentication, IIS can delegate security credentials among computers running Windows 2000 or later that are trusted and configured for delegation.”. Which means to get delegation to work you need to be using Kerberos and you need to go off and read another couple of articles which details how to set this up this msdn article and Q810572. The import part in these articles is if you’re running IIS5 in the default config “the account used to run the server process (the process that performs impersonation) is allowed to delegate client accounts. You must configure the user account under which the server process runs, or if the process runs under the local SYSTEM account.”. On a IIS5 box the default user is the aspnet user which means you will need to change the account being used to one that can be trusted for delegation (eg the system account).
(Note: this was one thing that I found really hard to get good accurate information on what I’ve included in this post is what I found worked for me. Running the asp.net process under the system account does open up certain security issues but hey so does enabling delegation on your web server if someone wishes’s to correct me please send me an email. If you’re using 2003 this conversation is kind of redundant plus you have the ability to do constrained delegation)
SSL and Self signed certificates
Running WebDAV over http remotely is not a great idea because your going to be shipping your data as clear text over the network So its best to use https to do all your conversations with the Exchange server. If you’re down the less financially able spectrum you may not be able to afford to be using registered SSL certificates. In this case your application may have to deal with certificate warning messages. You can do this “by implementing your own CertificatePolicy class (which implements the ICertificatePolicy interface). In this class you will have to write your own CheckValidationResult function that has to return true or false” have a read of http://weblogs.asp.net/jan/archive/2003/12/04/41154.aspx which gives a good description and some code to work around this.
Some Examples
I’ve come up with a couple of samples that demonstrate all the stuff I’ve talked about. Both samples do the same thing they use the System.Directroyservices namespace to build a URL to access the user’s logon information (based on the user that is authenticated on the IIS server). Once the URL is built it then connects to the mailbox and queries all the contacts in a user’s mailbox. The first example uses a XSL file to do a transform of the XML returned from the WebDAV query. They both use SSL and implement a method to override any certificate popup warnings. The second example loads the result of the WebDAV query into an ADO.NET dataset and then displays the result in a datagrid. Both code samples need to be located in a directory that has all other authentication except windows authentication turned off. I’ve posted a downloadable copy of both examples here the second example looks like.
<%@ Page %>
<%@Import namespace="System.Data"%>
<%@Import namespace="System.Net"%>
<%@Assembly name="System.DirectoryServices, Version=1.0.5000.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a, Custom=null"%>
<%@Import namespace="System.DirectoryServices"%>
<%@Import namespace="System.Security.Cryptography.X509Certificates"%>
<script language="vb" runat="server">
Public Class AcceptAllCertificatePolicy
Implements ICertificatePolicy
Public Overridable Function CheckValidationResult(ByVal srvPoint As ServicePoint,
ByVal certificate As X509Certificate, ByVal request As WebRequest, ByVal problem
As Integer) As Boolean Implements ICertificatePolicy.CheckValidationResult
Return True 'this accepts all certificates
End Function
End Class
Sub Page_Load(sender As Object, e As EventArgs)
System.Net.ServicePointManager.CertificatePolicy = New
AcceptAllCertificatePolicy
Dim Request As System.Net.HttpWebRequest
Dim Response As System.Net.HttpWebResponse
Dim strRootURI As String
Dim strQuery As String
Dim bytes() As Byte
Dim workrow As System.Data.DataRow
Dim resrow As System.Data.DataRow
Dim impersonationContext As
System.Security.Principal.WindowsImpersonationContext
Dim currentWindowsIdentity As System.Security.Principal.WindowsIdentity
currentWindowsIdentity = CType(User.Identity,
System.Security.Principal.WindowsIdentity)
impersonationContext = currentWindowsIdentity.Impersonate()
Dim MyCredentialCache As System.Net.CredentialCache
Dim resdataset As New System.Data.DataSet
Dim RequestStream As System.IO.Stream
Dim ResponseStream As System.IO.Stream
Dim ResponseXmlDoc As System.Xml.XmlDocument
Dim DisplayNameNodes As System.Xml.XmlNodeList
Dim objsearch As New System.DirectoryServices.DirectorySearcher
Dim strrootdse As String = objsearch.SearchRoot.Path
Dim objdirentry As New system.DirectoryServices.DirectoryEntry(strrootdse)
Dim objresult As system.DirectoryServices.SearchResult
Dim stremailaddress As String
Dim strhomeserver As String
objsearch.Filter = "(&(&(&(& (mailnickname=*) (|
(&(objectCategory=person)(objectClass=user)(|(homeMDB=*)" _
& "(msExchHomeServerName=*)))
)))(objectCategory=user)(userPrincipalName=*)(mailNickname=" &
System.Environment.UserName & ")))"
objsearch.SearchScope = DirectoryServices.SearchScope.Subtree
objsearch.PropertiesToLoad.Add("mail")
objsearch.PropertiesToLoad.Add("msExchHomeServerName")
objsearch.Sort.Direction = DirectoryServices.SortDirection.Ascending
objsearch.Sort.PropertyName = "mail"
Dim colresults As DirectoryServices.SearchResultCollection = objsearch.FindAll()
For Each objresult In colresults
stremailaddress = objresult.GetDirectoryEntry().Properties("mail").Value
strhomeserver =
objresult.GetDirectoryEntry().Properties("msExchHomeServerName").Value
Next
Dim emailNameNodes As System.Xml.XmlNodeList
strhomeserver = Right(strhomeserver, Len(strhomeserver) - (InStr(strhomeserver,
"cn=Servers/cn=") + 13))
strRootURI = "https://" & strhomeserver & "/exchange/" & stremailaddress &
"/contacts/"
strQuery = "<?xml version=""1.0""?>" & _
"<D:searchrequest xmlns:D = ""DAV:"" >" & _
"<D:sql>SELECT ""urn:schemas:contacts:cn"",
""http://schemas.microsoft.com/mapi/email1emailaddress"" " & _
"FROM """ & strRootURI & """" & _
"WHERE ""DAV:ishidden"" = false AND ""DAV:isfolder"" = false AND
""DAV:contentclass"" = 'urn:content-classes:person'" & _
"</D:sql></D:searchrequest>"
Request = CType(System.Net.WebRequest.Create(strRootURI), _
System.Net.HttpWebRequest)
Request.Credentials = System.Net.CredentialCache.DefaultCredentials
Request.Method = "SEARCH"
bytes = System.Text.Encoding.UTF8.GetBytes(strQuery)
Request.ContentLength = bytes.Length
RequestStream = Request.GetRequestStream()
RequestStream.Write(bytes, 0, bytes.Length)
RequestStream.Close()
Request.ContentType = "text/xml"
Request.Headers.Add("Translate", "F")
Response = CType(Request.GetResponse(), System.Net.HttpWebResponse)
ResponseStream = Response.GetResponseStream()
ResponseXmlDoc = New System.Xml.XmlDocument
ResponseXmlDoc.Load(ResponseStream)
DisplayNameNodes = ResponseXmlDoc.GetElementsByTagName("d:cn")
emailNameNodes = ResponseXmlDoc.GetElementsByTagName("e:email1emailaddress")
Dim resultstable As DataTable
resultstable=new DataTable()
resultstable.Columns.Add("Contact Name")
resultstable.Columns.Add("Email Address")
AddRow(resultstable, "glenscales@yahoo.com", "Tests")
If DisplayNameNodes.Count > 0 Then
Dim i As Integer
For i = 0 To DisplayNameNodes.Count - 1
AddRow(resultstable, emailNameNodes(i).InnerText, DisplayNameNodes(i).InnerText)
Next
Else
Console.WriteLine("No non-folder items found...")
End If
Showcontacts.DataSource=resultstable
Showcontacts.DataBind()
impersonationContext.Undo()
End Sub
Sub AddRow(resultstable As DataTable, Emailaddress As String, Name As String)
Dim row As DataRow
row=resultstable.NewRow()
row("Contact Name")=Name
row("Email Address")=Emailaddress
resultstable.Rows.Add(row)
End Sub
</script>
<HTML>
<body>
<form id="Recipe1416vb" method="post" runat="server">
<asp:DataGrid ID="Showcontacts" Runat="server" AutoGenerateColumns="False">
<Columns>
<asp:BoundColumn DataField="Contact Name" HeaderText="Contact Name" />
<asp:BoundColumn DataField="Email Address" HeaderText="Email Address" />
</Columns>
</asp:DataGrid>
</form>
</body>
</HTML>