tirsdag den 6. september 2011

Claimsbased authentication and WCF Services

Update 22-09-2011: Code samples posted here
In the last few months I’ve spend a lot of time messing with claimsbased authentication. a lot of it have been toward implementing single sign on though various social providers. But some of it have also been involving working with SharePoint 2010 and CRM 2011 and developing and calling my own WCF services from both applications and websites.

I wanted to document what I learn, as I learned it, on this blog but the time just hasn’t been on my side, but I do how ever spend a little time on a piece of code I wrote that I think a lot people will find useful and can copy’n’paste a bit from.

Most of the actions and information I need in different places are all wrapped up in 1 WCF service, I call this WCF service from both other WCF services and websites and from various applications. I found it was easier for me to just create an Class Library that could talk with this WCF service and then reference that from all my projects.

image

Most of the code is about handling active federation against any kind of claimsbased identity provider, but I also put in a bit of code to handle passive federation that is needed when working with SharePoint 2010 though code.

I have client class that in a simple way make authentication against an identity provider and receiving a claims token back. I want to go though how to use this client to get a token and what can happen, since I have seen A LOT of forum post from people getting these errors and not getting an answer that fits.

We start by creating an instance of the client and choosing what kind of encryption we want to use.

Code Snippet
  1. Dim ClaimsClient As New ClaimsAuth.Client
  2. ClaimsClient.TokenEncryption = Microsoft.IdentityModel.SecurityTokenService.KeyTypes.Symmetric

if we look in Microsoft.IdentityModel.SecurityTokenService.KeyTypes we see we can use Asymmetric, Symmetric or Bearer. Tons of post out there about this.

If you use Asymmetric you as requestor need to supply a key to encrypt the claims with. ( set "UseKey” )

If you use Symmetric the identity provider have all ready been told what certificate to use, to encrypt the claims with.

If you choose Bearer. The token get signed, but claims will not be encrypted. If a token signing certificate have been assigned on the Relying Party, claims will simply not be included at all.

When you request a token, the token gets signed (not encrypted) with a certificate installed on the Identity Provider ( ADFS ). If you add a certificate on a Relying Party Trust (RP) on the ADFS server, the claims inside the token gets encrypted with with that certificate. Only host/applications that have access to the private key of that certificate can now decrypt the token and read the claims. You don’t need to read the claims in order to authenticate your self. For instance if you have a WCF Service you want to call from within an application. You can from within that application still request a token from the ADFS server and then access the WCF service with that Token. As long as the WCF service have access to the private key and can read the claims, your application don’t need it.

If you choose Symmetric but the Relying Party on the ADFS have not been assigned a certificate to encrypt the claims with you will get

ID3037: The specified request failed.

and in the event log on the ADFS server they would also see

ID4007: The symmetric key inside the requested security token must be encrypted. To fix this, either override the SecurityTokenService.GetScope() method to assign appropriate value to Scope.EncryptingCredentials or set Scope.SymmetricKeyEncryptionRequired to false.

Either request a Bearer token or Asymmetric (not sure if you can this?) token, or add a certificate on the RP

imageimage

To make it simple. If you want to make absolutely sure your ADFS server only issues tokens to the hosts you have given the certificate with private key too, sign the tokens with this certificate by taking the public part of the certificate and save to a file and then assign it on this tab.
If you need to authenticate from many places and don’t want to struggle with distributing a certificate including its private key around. or if you don’t care others can read the claim ( you need to successfully authenticate in order to get the claims in the first place so in theory they should have access to it anyway ) leave this field blank.

To add the certificate to the ADFS server, on the computer you have the certificate you want to use for signing claims with, open certificates and add Local computer or user, depending on where you have the certificate installed. Right click it and choose open

image
Go to Details and click Copy to file

image

Accept the defaults and save the file. Then use this file when adding a certificate on the RP on the ADFS server.

If you choose to get an Bearer token you cannot reuse this key to authenticate to other RP’s by authentication with the issued token, That will fail with

The signing token XXXX has no keys. The security token is used in a context that requires it to perform cryptographic operations, but the token contains no cryptographic keys. Either the token type does not support cryptographic operations, or the particular token instance does not contain cryptographic keys. Check your configuration to ensure that cryptographically disabled token types (for example, UserNameSecurityToken) are not specified in a context that requires cryptographic operations (for example, an endorsing supporting token).

When you have a token and you want read the claims inside, you will often see errors like

ID4022: The key needed to decrypt the encrypted security token could not be resolved. Ensure that the SecurityTokenResolver is populated with the required key.

Or from a asp.net website

ID4036: The key needed to decrypt the encrypted security token could not be resolved from the following security key identifier 'XXXXX'. Ensure that the SecurityTokenResolver is populated with the required key.

I think its pretty self explaining but there's a ton of forum post’s out there where people ask for help. Again, you have a token, its valid, you can authenticate your self with it, but when you try to read the token you get the above error. You get it be course a certificate has been added on the RP on the ADFS and you haven't given WIF the certificate including private key, needed to decrypt it. If using my code, just load it and add it on the TokenSigningCertificate Property. If you see this error on a websites you are probably missing the serviceCertificate  in web.config

 

Code Snippet
  1. <microsoft.identityModel>
  2.   <service saveBootstrapTokens="true">
  3.     <certificateValidation certificateValidationMode="None" />
  4.     <serviceCertificate>
  5.       <certificateReference x509FindType="FindByThumbprint" findValue="7A41CF269D6BCDED80DDD9B6FD517E37891453B5" storeLocation="LocalMachine" storeName="My" />
  6.     </serviceCertificate>
  7.   </service>
  8. </microsoft.identityModel>

And while at those errors. if you get something down the line of

ID4175: The issuer of the security token was not recognized by the IssuerNameRegistry. To accept security tokens from this issuer, configure the IssuerNameRegistry to return a valid name for this issuer.

you are missing the certificate from the Identity Provider ( ADFS )

Code Snippet
  1. <microsoft.identityModel>
  2.   <service saveBootstrapTokens="true">
  3.     <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
  4.       <trustedIssuers>
  5.         <add thumbprint="86AC2E62900DF9451B0596562D52F7212AC31065" name="http://adfs.wingu.dk/adfs/services/trust" />
  6.       </trustedIssuers>
  7.     </issuerNameRegistry>
  8.   </service>
  9. </microsoft.identityModel>

Back to the code, Next I choose what I want to access (realm) and who to authenticate me (identity provider / ADFS) and how I want to authenticate

Code Snippet
  1. Dim ClaimsClient As New ClaimsAuth.Client
  2. ClaimsClient.TokenEncryption = Microsoft.IdentityModel.SecurityTokenService.KeyTypes.Symmetric
  3. ClaimsClient.IdentityProvider = "https://adfs.wingu.dk/"
  4. ClaimsClient.Realm = "https://admin.wingu.dk/ssi2/"
  5. ClaimsClient.authenticateBy = MessageCredentialType.Windows
  6. 'ClaimsClient.username = txtUsername.Text
  7. 'ClaimsClient.Password = txtPassword.Text
  8. 'ClaimsClient.ClientCertificate = MyPersonalCertificate
  9. 'ClaimsClient.IssuedToken = OtherIssuedToken
  10. 'Dim ClaimsID As Microsoft.IdentityModel.Claims.ClaimsIdentity = _
  11. 'DirectCast(HttpContext.Current.User.Identity, Microsoft.IdentityModel.Claims.ClaimsIdentity)
  12. 'Dim BootstrapToken As System.IdentityModel.Tokens.SecurityToken = ClaimsID.BootstrapToken
  13. 'ClaimsClient.ActAsToken = BootstrapToken
  14. ClaimsClient.authenticate()

AuthenticateBy can be either Certificate, IssuedToken, UserName or Windows. Just for fun I showed other ways to authenticate in the remarks. On that is particular interesting is the ActAs . This is what you would normally do from within an asp.net application that needs to call an WFC service on behalf of the user. Either Authenticate by Windows or username/password form within the asp.net application and then attach the user’s bootstrap token. you need permission to do this of course. That is what the Delegation Authorization Rules are for on the RP Claims rule dialog

image

Back to the code, so inside the client class I have my authenticate function.

Code Snippet
  1. Function authenticate() As System.IdentityModel.Tokens.SecurityToken
  2.     Select Case _authenticateBy
  3.         Case MessageCredentialType.UserName : _IssuedToken = GetADFSTokenUsernamemixed()
  4.         Case MessageCredentialType.Windows : _IssuedToken = GetADFSTokenKerberos()
  5.         Case MessageCredentialType.IssuedToken
  6.             If _IssuedToken Is Nothing Then Throw New Exception("No token found to issue new token with")
  7.             _IssuedToken = GetADFSTokenIssuedToken()
  8.         Case Else : Throw New Exception("unknown authentication schema")
  9.     End Select
  10.     Return _IssuedToken
  11. End Function

most of this code can be reused against any kind of Claimsbased authentication identity provider but for now I have only done the logic for ADFS and SharePoint.
GetADFSTokenUsernamemixed and GetADFSTokenKerberos is almost the same, GetADFSTokenIssuedToken is a bit more tricky

So here they are

Code Snippet
  1. Private Function GetADFSTokenUsernamemixed() As System.IdentityModel.Tokens.SecurityToken
  2.     Dim Token As System.IdentityModel.Tokens.SecurityToken
  3.     Dim UserNameMixed As String = _IdentityProvider & "adfs/services/trust/13/usernamemixed"
  4.     Dim STSbinding = New Microsoft.IdentityModel.Protocols.WSTrust.Bindings.UserNameWSTrustBinding
  5.     STSbinding.SecurityMode = SecurityMode.TransportWithMessageCredential
  6.     Dim trustChannelFactory As New WSTrustChannelFactory(STSbinding, New EndpointAddress(UserNameMixed))
  7.     trustChannelFactory.TrustVersion = System.ServiceModel.Security.TrustVersion.WSTrust13
  8.  
  9.     trustChannelFactory.Credentials.SupportInteractive = False
  10.     trustChannelFactory.Credentials.UserName.UserName = _Username
  11.     trustChannelFactory.Credentials.UserName.Password = _Password
  12.  
  13.     Try
  14.         Dim rst As New RequestSecurityToken()
  15.         rst.RequestType = WSTrust13Constants.RequestTypes.Issue
  16.         rst.AppliesTo = New EndpointAddress(_Realm)
  17.         rst.KeyType = _TokenEncryption
  18.         rst.TokenType = _TokenType
  19.  
  20.         If _ClientCertificate IsNot Nothing Then
  21.             Dim clause As System.IdentityModel.Tokens.SecurityKeyIdentifierClause = _
  22.                 New System.IdentityModel.Tokens.X509RawDataKeyIdentifierClause(_ClientCertificate)
  23.  
  24.             rst.UseKey = New UseKey(New System.IdentityModel.Tokens.SecurityKeyIdentifier(clause), _
  25.                                      New System.IdentityModel.Tokens.X509SecurityToken(_ClientCertificate))
  26.         End If
  27.  
  28.         'This part will give you identity of logged in user
  29.         If _ActAs IsNot Nothing Then rst.ActAs = _ActAs
  30.  
  31.         If _requestClaims.Count > 0 Then
  32.             For Each claim In _requestClaims
  33.                 rst.Claims.Add(claim)
  34.             Next
  35.         End If
  36.         Dim channel = trustChannelFactory.CreateChannel()
  37.         Dim rstr As RequestSecurityTokenResponse = Nothing
  38.         Token = channel.Issue(rst, rstr)
  39.     Catch ex As Exception
  40.         Throw New Exception(ex.Message, ex)
  41.     Finally
  42.         Try
  43.             If trustChannelFactory.State = CommunicationState.Faulted Then
  44.                 trustChannelFactory.Abort()
  45.             Else
  46.                 trustChannelFactory.Close()
  47.             End If
  48.         Catch generatedExceptionName As Exception
  49.         End Try
  50.     End Try
  51.     Return Token
  52. End Function
  53.  
  54. Private Function GetADFSTokenKerberos() As System.IdentityModel.Tokens.SecurityToken
  55.     Dim Token As System.IdentityModel.Tokens.SecurityToken
  56.     Dim KerberosMixed As String = _IdentityProvider & "adfs/services/trust/13/kerberosmixed"
  57.     Dim STSbinding = New Microsoft.IdentityModel.Protocols.WSTrust.Bindings.KerberosWSTrustBinding
  58.     STSbinding.SecurityMode = SecurityMode.TransportWithMessageCredential
  59.  
  60.     Dim trustChannelFactory As New WSTrustChannelFactory(STSbinding, New EndpointAddress(KerberosMixed))
  61.  
  62.     trustChannelFactory.TrustVersion = System.ServiceModel.Security.TrustVersion.WSTrust13
  63.     trustChannelFactory.Credentials.SupportInteractive = False
  64.     trustChannelFactory.Credentials.Windows.AllowedImpersonationLevel = TokenImpersonationLevel.Impersonation
  65.     trustChannelFactory.Credentials.Windows.ClientCredential = System.Net.CredentialCache.DefaultNetworkCredentials
  66.  
  67.     Try
  68.         Dim rst As New RequestSecurityToken()
  69.         rst.RequestType = WSTrust13Constants.RequestTypes.Issue
  70.         rst.AppliesTo = New EndpointAddress(_Realm)
  71.         rst.KeyType = _TokenEncryption
  72.         rst.TokenType = _TokenType
  73.         If _ActAs IsNot Nothing Then rst.ActAs = _ActAs
  74.  
  75.         If _requestClaims.Count > 0 Then
  76.             For Each claim In _requestClaims
  77.                 rst.Claims.Add(claim)
  78.             Next
  79.         End If
  80.         Dim channel = trustChannelFactory.CreateChannel()
  81.         Dim rstr As RequestSecurityTokenResponse = Nothing
  82.         Token = channel.Issue(rst, rstr)
  83.         Dim t = rstr.RequestedSecurityToken.SecurityToken
  84.         Dim s As String = ""
  85.  
  86.     Catch ex As Exception
  87.         Throw New Exception(ex.Message, ex)
  88.     Finally
  89.         Try
  90.             If trustChannelFactory.State = CommunicationState.Faulted Then
  91.                 trustChannelFactory.Abort()
  92.             Else
  93.                 trustChannelFactory.Close()
  94.             End If
  95.         Catch generatedExceptionName As Exception
  96.         End Try
  97.     End Try
  98.     Return Token
  99. End Function
  100.  
  101. Private Function GetADFSTokenIssuedToken() As System.IdentityModel.Tokens.SecurityToken
  102.     Try
  103.         Dim Token As System.IdentityModel.Tokens.SecurityToken
  104.         Dim IssuedtokenMixed As String = _IdentityProvider & "adfs/services/trust/13/issuedtokenmixedsymmetricbasic256"
  105.  
  106.         Dim binding = New Microsoft.IdentityModel.Protocols.WSTrust.Bindings.IssuedTokenWSTrustBinding()
  107.         binding.SecurityMode = SecurityMode.TransportWithMessageCredential
  108.  
  109.         Dim factory = New WSTrustChannelFactory(binding, New EndpointAddress(IssuedtokenMixed))
  110.         factory.TrustVersion = TrustVersion.WSTrust13
  111.         factory.Credentials.SupportInteractive = False
  112.  
  113.         Dim rst = New RequestSecurityToken() With { _
  114.          .RequestType = WSTrust13Constants.RequestTypes.Issue, _
  115.          .AppliesTo = New EndpointAddress(_Realm), _
  116.          .KeyType = WSTrust13Constants.KeyTypes.Symmetric _
  117.         }
  118.         rst.TokenType = _TokenType
  119.         rst.KeyType = _TokenEncryption  
  120.         factory.ConfigureChannelFactory()
  121.  
  122.         If _requestClaims.Count > 0 Then
  123.             For Each claim In _requestClaims
  124.                 rst.Claims.Add(claim)
  125.             Next
  126.         End If
  127.  
  128.         Dim channel = factory.CreateChannelWithIssuedToken(_IssuedToken)
  129.         Token = channel.Issue(rst)
  130.         Return Token
  131.     Catch ex As Exception
  132.         Throw ex
  133.     End Try
  134. End Function

I need to wrap up a few more loose ends and add a few comments but the class library and a simple test application will be available for download on this blog in a few days

søndag den 4. september 2011

Web services and wildcard certificate

I’m fiddling with web services that uses claims based authentication, and spend a few hours banging my head against the wall with this scenario.

Image you have the following code

 

Code Snippet
  1. Dim binding = New WS2007FederationHttpBinding(WSFederationHttpSecurityMode.TransportWithMessageCredential)
  2. binding.Security.Message.EstablishSecurityContext = False
  3. binding.Security.Mode = WSFederationHttpSecurityMode.Message
  4. Dim factory As New ChannelFactory(Of wsClaimsCloudAPI.ClaimsCloudAPIChannel)(binding, "http://admin.wingu.dk/CloudAPI/ClaimsCloudAPI.svc")
  5. factory.ConfigureChannelFactory()   '(Of wsClaimsCloudAPI.ClaimsCloudAPIChannel)()
  6. factory.Credentials.SupportInteractive = False
  7. Dim channel = factory.CreateChannelWithIssuedToken(Token)
  8. Dim s = channel.getToken
  9. MsgBox(s)
and get this error back

Identity check failed for outgoing message. The expected DNS identity of the remote endpoint was 'admin.wingu.dk' but the remote endpoint provided DNS claim 'wingu.dk'. If this is a legitimate remote endpoint, you can fix the problem by explicitly specifying DNS identity 'wingu.dk' as the Identity property of EndpointAddress when creating channel proxy.

m using a wildcard certificate on the webserver and I guess that’s what confusing things. Some people claim you can fix this by setting the host identity on the web service binding on the server, but that didn’t seem to work for me. but doesn’t matter, the error it self explains what to do. Explicitly specify the identity. so the code becomes

Code Snippet
  1. Dim EndpointURI As New Uri("http://admin.wingu.dk/CloudAPI/ClaimsCloudAPI.svc")
  2. Dim EndpointIdentity As EndpointIdentity = EndpointIdentity.CreateDnsIdentity("wingu.dk")
  3. Dim remoteAddress As New EndpointAddress(EndpointURI, EndpointIdentity, New System.ServiceModel.Channels.AddressHeaderCollection)
  4. Dim factory As New ChannelFactory(Of wsClaimsCloudAPI.ClaimsCloudAPIChannel)(binding, remoteAddress)

And everything works like a charm.

Note if using CreateChannelWithIssuedToken() and supply nothing or an illegal token, you will get back.

The address of the security token issuer is not specified. An explicit issuer address must be specified in the binding for target 'http://admin.wingu.dk/CloudAPI/ClaimsCloudAPI.svc' or the local issuer address must be configured in the credentials.

So make sure the token your testing with is valid.

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