torsdag den 14. juli 2011

Exchange 2010 Service pack 1 Hosting mode and Claims based authentication

I was a happy user of Windows Identity Foundation and ADFS 2.0 against out Exchange 2010 servers, so when messing about with service pack 1 I naturally also tried setting up the c2wts service and configurering claims based authentication up.
That isn't as easy as it sounds so here's a short guide. ( loosely based on information from this document and this guide )

Following this guide will “break” the ECP website. you need to run though all of this again on the Exchange Control Panel website. The difference from OWA to ECP is that you don’t need to “remark out” location.

Also, user controls will fail loading in ECP with an

WebHost failed to process a request.
Sender Information: System.ServiceModel.ServiceHostingEnvironment+HostingManager/59085005
Exception: System.ServiceModel.ServiceActivationException: The service '/ecp/RulesEditor/InboxRules.svc' cannot be activated due to an exception during compilation.  The exception message is: Required attribute 'name' not found. (C:\Program Files\Microsoft\Exchange Server\V14\ClientAccess\ecp\web.config line 1859). ---> System.Configuration.ConfigurationErrorsException: Required attribute 'name' not found. (C:\Program Files\Microsoft\Exchange Server\V14\ClientAccess\ecp\web.config line 1859)

To fix this, change <binding> to <binding name="ws2007Federation">

Download and install Microsoft Windows Identity Foundation and Windows Identity Foundation SDK on all CAS servers.

First off, the configuration utility gets massively confused over the web.config file, so first open C:\Program Files\Microsoft\Exchange Server\V14\Client Access\Owa\web.config and remark out the <location> tag. ( begin tag is at line 4, end tag is at line 26 )
image

Open Windows Identity Foundation Federation Utility and point to C:\Program Files\Microsoft\Exchange Server\V14\Client Access\Owa\web.config . Type in the external URL of your OWA site.
image
Type in metadata url for your ADFS server  ( for instance https://adfs.wingu.dk/FederationMetadata/2007-06/FederationMetadata.xml )
image
The rest is default.

Open web.config again and un-remark the location tags.
Add WIF modules to configuration –>system.webServer –> modules
Before:
<modules>
  <add type="Microsoft.Exchange.Clients.Owa.Core.OwaModule, Microsoft.Exchange.Clients.Owa" name="OwaModule" />
  <add name="exppw" />
</modules>
After:
<modules runAllManagedModulesForAllRequests="true">
  <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler"/>
  <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler"/>
  <add type="Microsoft.Exchange.Clients.Owa.Core.OwaModule, Microsoft.Exchange.Clients.Owa" name="OwaModule" />
  <add name="exppw" />
</modules>

Force users to be authenticated.
configuration-> system.web –> Add the following

<authorization>
  <deny users="?"/>
</authorization>

Enable UPN. configuration –> system.serviceModel –> bindings –> ws2007FederationHttpBinding –> binding –> security –> message –> claimTypeRequirements. Unmark UPN
<add claimType="http://schemas.xmlsoap.org/claims/UPN" isOptional="true" />

Tell WIF to create a Windows Token instead of passing the SAML token to OWA.
microsoft.identityModel –>service->  Add

<securityTokenHandlers>
  <add type="Microsoft.IdentityModel.Tokens.Saml11.Saml11SecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
    <samlSecurityTokenRequirement mapToWindows="true" useWindowsTokenService="true"/>
  </add>
</securityTokenHandlers>

Tell WIF to redirct users to your STS /ADFS,
microsoft.identityModel –>service-> Add

<federatedAuthentication>
  <wsFederation passiveRedirectEnabled="true" issuer="https://adfs.wingu.dk/adfs/ls/" realm="https://test01exc01.test01.local/owa/" requireHttps="true"/>
  <cookieHandler requireSsl="true"/>
</federatedAuthentication>

Open a Exchange powershell console and run

get-owavirtualdirectory | Set-owavirtualdirectory -FormsAuthentication:$false
get-owavirtualdirectory | Set-OwaVirtualDirectory -WindowsAuthentication $true
iisreset /noforce

Lastly, enable c2wt, by first openinig C:\Program Files\Windows Identity Foundation\v3.5\c2wtshost.exe.config and umark <add value="NT AUTHORITY\System" />

Open Services and set “Claims to Windows Token Service” service to start automatic (start/restart the service now)

Copy C:\Program Files\Microsoft\Exchange Server\V14\ClientAccess\Owa\FederationMetadata\2007-06\FederationMetadata.xml to your ADFS server and add owa as an relying Party Trust.
Add the following to Rules.
Pass Through or Filter an Incomming Claim –> UPN
image
Transform an Incomming Claim –> E-Mail Address –> UPN
image

Open properties for your new relying part and change –> Advanced SHA-1

image

Encryption,. remove it ( if the WIF wizard forced you to choose one )

image
EndPoints –> Add a WS-Federation endpoint

image

onsdag den 13. juli 2011

Exchange 2010 SP1 hosting mode import user

I’m preparing for Exchange 2010 with service pack 1 in hosting mode. There is several new things to take into account, but one of the first things I ran into that was driving me crazy was how to “import” an existing user to an exchange Organization.

I started with some nasty PowerShell script I got off from a forum post but the result wasn’t really that good, but then I stumbled across the HMC migration tools for exchange 2010 sp1 and that got me kick started pretty well.

what I'm basically doing is first “importing” the user as a MailUser. This makes all the PowerShell commands “know” about the user and what Organization the user belongs and then we can assign a mailbox to the user. Complete with the correct plans etc.

You could use the scripts from the first forum post, and then mail-enable the user. That works, true. But the user wont have had the correct mailbox plan applied, and if you try Set-Mailbox -Identity $usermb -MailboxPlan $mailboxplan on the user you will get all kinds of annoying errors and warnings, so this is a lot cleaner. Now I just need to automate the process of getting legacyExchangeDN and adding it as an x500 address on the users after my “migration” ( Microsoft, you f***ing morrons creating a service pack that require us to uninstall exchange complete before applying Service pack 1)

# Init
$Org = $customer.code
$Organization = Get-Organization $customer.Code -ea 0
if(!$Organization){
$Organization = New-Organization -Name $customer.Code -DomainName $customer.PrimaryDomainName -Location da-DK -ProgramID HostingSample -OfferID 2
}

$ADOrg = [ADSI]("LDAP://" + $Organization.DistinguishedName)
$ExchCU = $ADOrg.msexchcu
$ExchOURoot = $ADOrg.msexchouroot

$UserAccountControlValue = 66048

$mailboxplan = get-mailboxplan -organization $Org | where-object {$_.isdefault -ilike ("true") }
$defmailboxplan = $mailboxplan.name

# Move user if needed
$ADUser = Get-ADUser $User.Username
$userDN = ("CN=" + $user.name + "," + $Organization.DistinguishedName)
if($ADUser.DistinguishedName -ne $userDN){
Move
-ADObject -Identity $ADUser.DistinguishedName -TargetPath $Organization.DistinguishedName -Server $PreferedDC
}

#Assign user to exchange Organization
$objUser = [ADSI]("LDAP://" + $ADUser.DistinguishedName)
$objUser.Put("msExchCU","$ExchCU")
$objUser.Put("msExchOURoot","$ExchOURoot")
$objUser.setInfo()

# Create mailbox if needed
$usermb = get-mailbox $ADUser.DistinguishedName -ea 0
if(!$usermb){
$temp = Enable-MailUser $ADUser.DistinguishedName -ExternalEmailAddress:$user.UPN
$temp | Enable-Mailbox
Set
-MailUser $ADUser.DistinguishedName -UserPrincipalName:$user.UPN
$usermb = get-mailbox $ADUser.DistinguishedName
}
$temp = Get-MailUser $user.UPN -Organization test02 -ea 0
if($temp){
$temp | Enable-Mailbox
$usermb = get-mailbox $ADUser.DistinguishedName
}

# Assign MailPlan
Set-Mailbox -Identity $usermb -MailboxPlan $mailboxplan

søndag den 3. juli 2011

SharePoint 2010 Managed Client Object Model and Claims based authentication

Update 22-09-2011: There is a more clean way to do this here
What a pain, this was. A client asked if I had some demo code for how to upload a file into SharePoint 2010. I though to my self, how hard can it be ? and went to it.

First thing you’ll run into is knowing how to even talk with SharePoint. there's a few but Google quickly lead me to SharePoint Foundation 2010 Managed Client Object Model And you download it here. That’s all nice and easy when using windows authentication or Forms based authentication. But if your using claims based authentication (like we are and Microsoft Online Services ) you wont find many examples out there.

I was struggling for a long time with this and everything I searched for kept getting me back to ClientOmAuth but its C# and I didn’t have a lot of luck with the initial copy’n’pasting to VB but after trying some other approaches that didn’t lead me anywhere good I went back to the above code and gave it a shot. So here's a VB.NET version supporting both Windows Authentication ( adfs/services/trust/13/windowstransport ) and username/password ( adfs/services/trust/13/usernamemixed ). Windows Authentication require you enable windowstransport  on the STS / ADFS server.

Imports Microsoft.IdentityModel.Protocols.WSTrust
Imports System.Security.Principal

Imports System.ServiceModel
Imports System.ServiceModel.Channels

Imports System.Net.Security
Imports System.Net
Imports System.IO
Imports System.Text
Imports System.Xml

Public Class SPAuth

    Private SPSUrl As String
    Private ADFSUrl As String
    Private _SAMLToken As String
    Private _Username As String
    Private _Password As String

    Public ReadOnly Property samlUri() As Uri
        Get
            Return New Uri(SPSUrl)
        End Get
    End Property

    Public ReadOnly Property SAMLToken() As String
        Get
            Return _SAMLToken
        End Get
    End Property

    Public WriteOnly Property username As String
        Set(value As String)
            _Username = value
        End Set
    End Property

    Public WriteOnly Property Password As String
        Set(value As String)
            _Password = value
        End Set
    End Property

    Sub New(SPSUrl As String, ADFSUrl As String, username As String, password As String)
        _Username = username
        _Password = password
        Me.SPSUrl = SPSUrl
        Me.ADFSUrl = ADFSUrl
        _SAMLToken = GetNewSamlToken()
    End Sub

    Sub New(SPSUrl As String, ADFSUrl As String)
        Me.SPSUrl = SPSUrl
        Me.ADFSUrl = ADFSUrl
        _SAMLToken = GetNewSamlToken()
    End Sub

    Private Function GetNewSamlToken() As String
        Dim ret As String = String.Empty

        Try
            Dim samlServer As String = If(SPSUrl.EndsWith("/"), SPSUrl, SPSUrl + "/")

            Dim sharepointSite = New With { _
             Key .Wctx = samlServer & "_layouts/Authenticate.aspx?Source=%2F", _
             Key .Wtrealm = samlServer, _
             Key .Wreply = samlServer & "_trust/" _
            }

            Dim stsServer As String = If(ADFSUrl.EndsWith("/"), ADFSUrl, ADFSUrl + "/")
            Dim stsUrl As String = stsServer & "adfs/services/trust/13/windowstransport"

            'get token from STS
            Dim stsResponse As String = GetResponse(sharepointSite.Wreply)

            'generate response to Sharepoint
            Dim stringData As String = [String].Format("wa=wsignin1.0&wctx={0}&wresult={1}", System.Web.HttpUtility.UrlEncode(sharepointSite.Wctx), System.Web.HttpUtility.UrlEncode(stsResponse))
            Dim sharepointRequest As HttpWebRequest = TryCast(HttpWebRequest.Create(sharepointSite.Wreply), HttpWebRequest)
            sharepointRequest.Method = "POST"
            sharepointRequest.ContentType = "application/x-www-form-urlencoded"
            sharepointRequest.CookieContainer = New CookieContainer()
            sharepointRequest.AllowAutoRedirect = False
            ' This is important
            Dim newStream As Stream = sharepointRequest.GetRequestStream()

            Dim data As Byte() = Encoding.UTF8.GetBytes(stringData)
            newStream.Write(data, 0, data.Length)
            newStream.Close()
            Dim webResponse As HttpWebResponse = TryCast(sharepointRequest.GetResponse(), HttpWebResponse)
            ret = webResponse.Cookies("FedAuth").Value
        Catch ex As Exception
            MessageBox.Show("Error: " + ex.Message)
        End Try

        Return ret
    End Function

    Private Function GetResponse(realm As String) As String

        Dim rst As New RequestSecurityToken()
        rst.RequestType = WSTrust13Constants.RequestTypes.Issue


        'bearer token, no encryption
        rst.AppliesTo = New EndpointAddress(realm)
        'rst.KeyType = WSTrustFeb2005Constants.KeyTypes.Bearer;
        rst.KeyType = WSTrust13Constants.KeyTypes.Bearer

        Dim stsServer As String = If(ADFSUrl.EndsWith("/"), ADFSUrl, ADFSUrl + "/")
        Dim stsUrl As String = stsServer & "adfs/services/trust/13/windowstransport"

        'WSTrustFeb2005RequestSerializer trustSerializer = new WSTrustFeb2005RequestSerializer();
        Dim trustSerializer As New WSTrust13RequestSerializer()
        Dim binding As New WSHttpBinding()
        If _Username <> "" And _Password <> "" Then
            stsUrl = stsServer & "adfs/services/trust/13/usernamemixed"
            binding.Security.Mode = SecurityMode.TransportWithMessageCredential
            binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName
            binding.Security.Message.EstablishSecurityContext = False
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic


        Else
            binding.Security.Mode = SecurityMode.Transport
            binding.Security.Message.ClientCredentialType = MessageCredentialType.None
            binding.Security.Message.EstablishSecurityContext = False
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows
        End If

        Dim address As New EndpointAddress(stsUrl)
        'WSTrustFeb2005ContractClient trustClient = new WSTrustFeb2005ContractClient(binding, address);
        Dim trustClient As New WSTrust13ContractClient(binding, address)
        If _Username <> "" And _Password <> "" Then
            trustClient.ClientCredentials.UserName.UserName = _Username
            trustClient.ClientCredentials.UserName.Password = _Password
        Else
            trustClient.ClientCredentials.Windows.AllowNtlm = True
            trustClient.ClientCredentials.Windows.AllowedImpersonationLevel = TokenImpersonationLevel.Impersonation
            trustClient.ClientCredentials.Windows.ClientCredential = CredentialCache.DefaultNetworkCredentials
        End If

        'MessageVersion.Default, WSTrustFeb2005Constants.Actions.Issue,
        Dim response As System.ServiceModel.Channels.Message = trustClient.EndIssue(trustClient.BeginIssue(System.ServiceModel.Channels.Message.CreateMessage(MessageVersion.[Default], WSTrust13Constants.Actions.Issue, New RequestBodyWriter(trustSerializer, rst)), Nothing, Nothing))
        trustClient.Close()

        Dim reader As XmlDictionaryReader = response.GetReaderAtBodyContents()
        Return reader.ReadOuterXml()
    End Function

    Public Sub clientContext_ExecutingWebRequest(sender As Object, e As Microsoft.SharePoint.Client.WebRequestEventArgs)
        Dim cc As New CookieContainer
        Dim samlAuth As New Cookie("FedAuth", SAMLToken)
        samlAuth.Expires = DateTime.Now.AddHours(1)

        samlAuth.Path = "/"
        samlAuth.Secure = True
        samlAuth.HttpOnly = True
        samlAuth.Domain = samlUri.Host
        cc.Add(samlAuth)
        e.WebRequestExecutor.WebRequest.CookieContainer = cc
        'e.WebRequestExecutor.WebRequest.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f")
    End Sub

End Class

<ServiceContract()> _
Public Interface IWSTrust13Contract
    <OperationContract(ProtectionLevel:=ProtectionLevel.EncryptAndSign, Action:="http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue", ReplyAction:="http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal", AsyncPattern:=True)> _
    Function BeginIssue(request As System.ServiceModel.Channels.Message, callback As AsyncCallback, state As Object) As IAsyncResult
    Function EndIssue(asyncResult As IAsyncResult) As System.ServiceModel.Channels.Message
End Interface

Partial Public Class WSTrust13ContractClient
    Inherits ClientBase(Of IWSTrust13Contract)
    Implements IWSTrust13Contract

    Public Sub New(binding As System.ServiceModel.Channels.Binding, remoteAddress As System.ServiceModel.EndpointAddress)
        MyBase.New(binding, remoteAddress)
    End Sub

    Public Function BeginIssue(request As System.ServiceModel.Channels.Message, callback As System.AsyncCallback, state As Object) As System.IAsyncResult Implements IWSTrust13Contract.BeginIssue
        Return MyBase.Channel.BeginIssue(request, callback, state)
    End Function

    Public Function EndIssue(asyncResult As System.IAsyncResult) As System.ServiceModel.Channels.Message Implements IWSTrust13Contract.EndIssue
        Return MyBase.Channel.EndIssue(asyncResult)
    End Function
End Class
So when you need to talk with your SharePoint you just type
' Use windows login (Kerberose)
' Dim SPAuth As New SPAuth("https://somesite.portal.domain.com", "https://adfs.domain.com")

' Use FBA, send username and password
Dim SPAuth As New SPAuth("https://somesite.portal.domain.com", "https://adfs.domain.com", "username@domain.com", "Sup3rS3cret")

Dim clientContext As New ClientContext("https://somesite.portal.domain.com/")
AddHandler clientContext.ExecutingWebRequest, AddressOf SPAuth.clientContext_ExecutingWebRequest
clientContext.Credentials = CredentialCache.DefaultCredentials
CurrentSite = clientContext.Web
clientContext.Load(CurrentSite)
clientContext.ExecuteQuery()

SharePoint 2010 Client and OpenBinaryDirect

Update 22-09-2011: There is a much better way to handle this here
So now we know how to get and update information. Now its time to figure out how to upload and download files from SharePoint. All the examples you’ll find out there reference the above command, OpenBinaryDirect. Well, it doesn’t work when using the trick mentioned in my last blog post but I found a good work around.

Someone at stackoverflow gave an example on how to access cookiecontainer in webclient, and that was just what I needed to my downloads.
( the complete project can be downloaded here )

Imports System.Net

Public Class CookieAwareWebClient
    Inherits WebClient
    Private CookieJar As New CookieContainer()
    Private _domain As String
    Public Property Domain() As String
        Get
            Return _domain
        End Get
        Set(ByVal value As String)
            _domain = value
            NewCookieContainer()
        End Set
    End Property

    Private _SAMLToken As String
    Public Property SAMLToken() As String
        Get
            Return _SAMLToken
        End Get
        Set(ByVal value As String)
            _SAMLToken = value
            NEwCookieContainer()
        End Set
    End Property

    Sub NewCookieContainer()
        If _domain <> "" And _SAMLToken <> "" Then
            Dim samlAuth As New Cookie("FedAuth", SAMLToken)
            samlAuth.Expires = DateTime.Now.AddHours(1)
            samlAuth.Path = "/"
            samlAuth.Secure = True
            samlAuth.HttpOnly = True
            samlAuth.Domain = _domain
            CookieJar = New CookieContainer()
            CookieJar.Add(samlAuth)
        End If
    End Sub

    Protected Overrides Function GetWebRequest(address As Uri) As WebRequest
        Dim request As WebRequest = MyBase.GetWebRequest(address)
        If TypeOf request Is HttpWebRequest Then
            TryCast(request, HttpWebRequest).CookieContainer = CookieJar
        End If
        Return request
    End Function
End Class

And then after getting an item you can download it with

Dim cli As New CookieAwareWebClient
cli.Domain = SPAuth.samlUri.Host
cli.SAMLToken = SPAuth.SAMLToken

Dim Uri As New Uri("https://" & cli.Domain & "/" & CurrentListItem("FileRef"))
Dim filename As String = Mid(CurrentListItem("FileRef"), InStrRev(CurrentListItem("FileRef"), "/") + 1)
cli.DownloadFile(Uri, "c:\" & filename)

And finally, heres how to upload without using SaveBinaryDirect who also failed with 403 on sites only configured for claims based authentication.

OpenFileDialog1.ShowDialog()
If OpenFileDialog1.FileName <> "" Then

    Dim listName As String = CurrentList.Title
    Dim filePath As String = OpenFileDialog1.FileName
    Dim filename As String = Mid(filePath, InStrRev(filePath, "\") + 1)
    'Dim fs As New FileStream(filePath, FileMode.Open)
    'Microsoft.SharePoint.Client.File.SaveBinaryDirect(clientContext, "/" & s & "/" & filename, fs, True)  '"/Shared Documents/" + filename, fs, True)

    Dim newFile As FileCreationInformation = New FileCreationInformation
    newFile.Content = System.IO.File.ReadAllBytes(filePath)
    newFile.Url = "/" & listName & "/" + filename
    newFile.Overwrite = True
    Dim uploadFile As Microsoft.SharePoint.Client.File = CurrentList.RootFolder.Files.Add(newFile)
    clientContext.Load(uploadFile)
    clientContext.ExecuteQuery()
    ' Set MetaData
    'Dim item As ListItem = uploadFile.ListItemAllFields
    'item("Title") = "TEST " & filename
    'item.Update()
    'clientContext.ExecuteQuery()
End If