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
            Return New Uri(SPSUrl)
        End Get
    End Property

    Public ReadOnly Property SAMLToken() As String
            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

            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)
            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

            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
            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))

        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
        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

Ingen kommentarer:

Send en kommentar