mandag den 25. februar 2013

ASP.NET and/or MVC websites and Claims based Authentication

Implementing WIF on a website is pretty straight forward. There are ton’s of guide out there on how to do it …. By doing a bit of magic in web.config you can make any website support claims based authentication. ( even Exchange 2010 and Citrix  )

If you are a developer, implementing WIF on your website is just as easy. You install WIF SDK and run FedUtil.exe who will handle configuring your web.config for you …

But what if you want absolute control over the login process. You often end up, with a mix of stuff in web.config and some in code. So I decided to figure out, what it would take, to support getting a user signed on, without ever touching web.config.

You will need to handle 4 actions. Requesting login, Handle login, Handle sign-out and handling Cleanup. So lets start with requesting login. But before we can do that, we need to have a basic setup of WIF. WIF normally do most of it’s magic though 3-4 modules, that get’s its configuration though web.config, and is saved in a static (shared) variable you can access simply by typing

Dim fam As Microsoft.IdentityModel.Web.WSFederationAuthenticationModule = Microsoft.IdentityModel.Web.FederatedAuthentication.WSFederationAuthenticationModule

But we want to control. To make things simple, we function like this

Public Function GetAuthenticationModule() As WSFederationAuthenticationModule
Dim fam As New WSFederationAuthenticationModule
fam.ServiceConfiguration = New Microsoft.IdentityModel.Configuration.ServiceConfiguration()
' use this, to allow any realm
' fam.ServiceConfiguration.AudienceRestriction.AudienceMode = IdentityModel.Selectors.AudienceUriMode.Never
' Add known Audience's ( Realms )
' This would proberly come from a database or what ever
For Each Realm in my.settings.Realms
Dim AllowedUri = fam.ServiceConfiguration.AudienceRestriction.AllowedAudienceUris.Where(Function(x) x.AbsoluteUri = IDP.TargetRealm).FirstOrDefault()
If AllowedUri Is Nothing Then
fam.ServiceConfiguration.AudienceRestriction.AllowedAudienceUris.Add(New Uri(IDP.TargetRealm))
End If
Next
fam.ServiceConfiguration.IssuerNameRegistry = New CustomTrustedIssuerNameRegistry()

Dim certificate As System.Security.Cryptography.X509Certificates.X509Certificate2 = CustomSecurityTokenServiceConfiguration.Current.EncryptingCertificate
AttachCert(fam.ServiceConfiguration, certificate)
' If you later decide to let WIF set the identity and save a FedAuth cookie, use thise to allow it to work incase you got
' your website hosted in Azure. The reason is Azure's Loadbalencing are using true round robin and doesnt support Sticky sessions.
' So in order to make all websites able to read the cookie, you need to a "common" ground for reading the encrypted token
'Dim sessionTransforms As New List(Of CookieTransform)(New CookieTransform() {New DeflateCookieTransform(), New RsaEncryptionCookieTransform(certificate),
' New RsaSignatureCookieTransform(certificate)})
'Dim sessionHandler As New Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler(sessionTransforms.AsReadOnly())
'fam.ServiceConfiguration.SecurityTokenHandlers.AddOrReplace(sessionHandler)
Return fam
End Function

Private Function InlineAssignHelper(Of T)(ByRef target As T, value As T) As T
target = value
Return value
End Function
Sub AttachCert(configuration As Microsoft.IdentityModel.Configuration.ServiceConfiguration, certificate As System.Security.Cryptography.X509Certificates.X509Certificate2)
configuration.ServiceCertificate = certificate
Dim certificates = New List(Of System.IdentityModel.Tokens.SecurityToken)() From { _
New System.IdentityModel.Tokens.X509SecurityToken(Configuration.ServiceCertificate) _
}
Dim encryptedSecurityTokenHandler = TryCast((From handler In Configuration.SecurityTokenHandlers Where TypeOf handler Is Microsoft.IdentityModel.Tokens.EncryptedSecurityTokenHandler).First(), Microsoft.IdentityModel.Tokens.EncryptedSecurityTokenHandler)
configuration.ServiceTokenResolver = InlineAssignHelper(encryptedSecurityTokenHandler.Configuration.ServiceTokenResolver, System.IdentityModel.Selectors.SecurityTokenResolver.CreateDefaultSecurityTokenResolver(certificates.AsReadOnly(), False))
End Sub

We need to tell WIF the realm(s) we are running under right now, called Audience Uri.
We need to tell WIF what signing certificate's to allow tokens from, called Issuer Name Registry.
CustomTrustedIssuerNameRegistry is a class that will get “fed” the signing certificates when WIF is validating the tokens, and should return a name (any name) if the certificate was allowed. Code for that is further down


Perfect, so now we are ready to create a sign request ( what happens when a user clicks the “login” button or what ever)

Public Function CreateSignInRequestMessage(Issuer As String, Realm As String, Reply As String) As SignInRequestMessage
Dim fam = GetAuthenticationModule()
fam.Issuer = Issuer
fam.Realm = Realm
Dim signInRequest = New SignInRequestMessage(New Uri(fam.Issuer), fam.Realm) With { _
.AuthenticationType = fam.AuthenticationType, _
.Freshness = fam.Freshness, .Reply = Reply
}
Return signInRequest
End Function

So in the code for your “login button” you do something down the line of this


Dim STSLoginPage as string = "https://adfs.wingu.dk/adfs/ls/"
' This webpage, must be in the AllowedAudienceUris
Dim TargetRealm as string = "https://domain.com/MyAwsomeWebapp"
Dim returnURL as string = "https://domain.com/MyAwsomeWebapp/login.aspx"
' or if using MVC
Dim returnURL as string = "https://domain.com/MyAwsomeWebapp/login/signin/"

Dim signInRequest = WIFHelper.CreateSignInRequestMessage(STSLoginPage, TargetRealm, returnURL)
Dim redirURL As String = signInRequest.WriteQueryString()
Response.Redirect(redirURL)

That will kick the user over to the identity provider, and if all goes well, he will then return with a POST to the return URL, where we can now pickup he’s SAML token.
So on the login page (or MVC controller) its now time to read the token, we add a function to our module, than handles that part


Private Function GetSignInResponseMessage() As SignInResponseMessage
Dim ctx = System.Web.HttpContext.Current
Dim req = ctx.Request
Dim message As SignInResponseMessage = WSFederationMessage.CreateFromFormPost(req) 'as SignInResponseMessage
Return message
End Function

Public Function GetWIFPrincipal(ByRef Issuer As String) As Microsoft.IdentityModel.Claims.ClaimsPrincipal 'System.Security.Principal.IPrincipal
Dim fam = GetAuthenticationModule()
Dim message As SignInResponseMessage = GetSignInResponseMessage()
'Dim token As System.IdentityModel.Tokens.SamlSecurityToken = fam.GetSecurityToken(message)
Dim token = fam.GetSecurityToken(message)
If token IsNot Nothing Then
Dim claims As Microsoft.IdentityModel.Claims.ClaimsIdentityCollection = fam.ServiceConfiguration.SecurityTokenHandlers.ValidateToken(token)
Dim principal As System.Security.Principal.IPrincipal = New Microsoft.IdentityModel.Claims.ClaimsPrincipal(claims)
If TypeOf token Is System.IdentityModel.Tokens.SamlSecurityToken Then
Dim t As System.IdentityModel.Tokens.SamlSecurityToken = token
Issuer = t.Assertion.Issuer
ElseIf TypeOf token Is Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityToken Then
Dim t As Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityToken = token
Issuer = t.Assertion.Issuer.Value
Else
Throw New Exception("Failed resolving SecurityToken to a known type '" & token.GetType.Name & "'")
End If
Return principal
End If
Return Nothing
End Function

And in the login page/controller you, can now either set the current principal, or parse the claims and sign in the user with formsbased authentication or what ever makes you tick.

Dim Issuer As String = Nothing
Dim Principal As Microsoft.IdentityModel.Claims.ClaimsPrincipal = GetWIFPrincipal(Issuer)
If Principal Is Nothing Then Throw New Exception("Failed parsing sign message to new Principal")
Dim Identity As Microsoft.IdentityModel.Claims.ClaimsIdentity = Principal.Identity
If Identity Is Nothing Then Throw New Exception("Failed parsing sign message to new Principal")
Issuer = Issuer.ToLower

Lastly, to make everything work correctly, we need to make our “login” page support at least 3 of the WS-Federation commands ( I guess that makes “login” page a wrong name then ? ) . So to wrap it all up, you will get something like this

Dim action as string = Request.QueryString(WSFederationConstants.Parameters.Action)
Select Case action
Case WSFederationConstants.Actions.SignIn
' Send user to the identity provider
Dim STSLoginPage As String = "https://adfs.wingu.dk/adfs/ls/"
' This webpage, must be in the AllowedAudienceUris
Dim TargetRealm As String = "https://domain.com/MyAwsomeWebapp"
Dim returnURL As String = "https://domain.com/MyAwsomeWebapp/login.aspx"
' or if using MVC
returnURL = "https://domain.com/MyAwsomeWebapp/login/signin/"
Case WSFederationConstants.Actions.SignOut
Dim requestMessage As SignOutRequestMessage = Nothing
Try
' This will fail, if a "sharepoint" login message
requestMessage = WSFederationMessage.CreateFromUri(Request.Url)
Catch ex As Exception
' ignore
End Try
' Return user to front page
model.RedirectURL = Request.Url.Scheme & "://" & Request.Url.Host & Request.ApplicationPath
If Not requestMessage Is Nothing Then
If Not String.IsNullOrEmpty(requestMessage.Reply) Then
' unless they logged out from another site, then send them back there
model.RedirectURL = requestMessage.Reply
End If
End If
FormsAuthentication.SignOut()
Session.Abandon()

' clear authentication cookie
Dim cookie1 As HttpCookie = New HttpCookie(FormsAuthentication.FormsCookieName, "")
cookie1.Expires = DateTime.Now.AddYears(-1)
Response.Cookies.Add(cookie1)

' clear session cookie
Dim cookie2 As HttpCookie = New HttpCookie("ASP.NET_SessionId", "")
cookie2.Expires = DateTime.Now.AddYears(-1)
Response.Cookies.Add(cookie2)
Case WSFederationConstants.Actions.SignOutCleanup
' Do the same as above, but send an "ok" image back
End Select
And lastly, we need to create class that validates singing certificates. 
Imports Microsoft.IdentityModel.Tokens
Imports System.IdentityModel.Tokens

Public Class CustomTrustedIssuerNameRegistry
Inherits IssuerNameRegistry
'Inherits Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry

Public Sub New()
End Sub
'Public Sub New(customConfiguration As System.Xml.XmlNodeList)
' MyBase.New(customConfiguration)
'End Sub

Public Overrides Function GetWindowsIssuerName() As String
Return MyBase.GetWindowsIssuerName()
End Function

Public Overloads Overrides Function GetIssuerName(securityToken As IdentityModel.Tokens.SecurityToken) As String
Dim x509Token As X509SecurityToken = TryCast(securityToken, X509SecurityToken)
If x509Token IsNot Nothing Then

For Each Thumbprint in My.Settings.AllowedSigningThumbprints
If [String].Equals(x509Token.Certificate.Thumbprint, Thumbprint) Then
Return x509Token.Certificate.SubjectName.Name
End If
Next
End If
Throw New SecurityTokenException("Untrusted issuer.")
End Function

End Class

lørdag den 23. februar 2013

SharePoint/CRM/Exchange and other Claims based websites with Social Sign in

The last week I have been working on a project, regarding combining Claims based authentication and login using Social Providers. In the cloud control panel that I normal work on, I have implemented support for signing in using Office 365, Azure AD, pass-through using Azure ACS, and the mandatory Open ID, Google ID, Windows Live ID, LinkedIn, Facebook, and Twitter. But for reasons I don’t want go into, a need came up, to create a simple STS (Secure Token Service), than would “tie” together one or more identities from social network, and offer those to websites and applications supporting WIF/Claims based authentication.

Why a new STS
If you implemented Social Network, either directly, or indirectly though ACS you probably noticed that if a user sign in using Google ID they will not be the same user, if they later decide to sign in using Facebook. That is relatively easy to “handle” if you wrote the application your self, and Microsoft did a decent job, tying it all up, using the membership provider together with DotNetOpenAuth in MVC 4, but if you don’t want to mess with wrapping up your own customer member ship provider, or if your facilitating WIF in Exchange/CRM/SharePoint/Navision or what ever, wouldn’t it just be awesome if you could just get it all “out of the box” some where. And what if that Service would also allow you to setup your own Identity Providers, so you could implement sign in using Office 365/Azure AD, your own ADFS server, or any other Secure Token Service you might have. You can now

The basic setup
We are still figuring out license models and such, but for now were thinking either a hosted solution, and one where you get the website to run for your self, lets image a hosted setup, and after that you get billed a small amount for each 100k users or something like that. So let’s imaging you want to implement this with Microsoft CRM 2011.

  • First signup for an account, you then get access to a Manage menu. But before you go there, head to CRM and start the “Configure Claims-Based Authentication” wizard
    image
    Type in the URL of the Secure Token Service
    image
    Select a certificate to use, when signing the Claims inside the tokens (this certificate needs to exists on all your CRM servers if you got more than one. It can be a self signed, from your internal CA, or one you bought from a third party
    image
    Go though the rest. Go though the “Configure Internet-Facing Deployment” if you haven't don’t so already, and at the end note the URL to the CRM servers metadata.xml file
  • Go to the STS, click Relays, past in the URL from the “Create new relying part” field, and press Create. By default all Social connectors will be allowed, but if you only want for instance Live ID, select that under Identity Providers, and hit Save.

And that is that. SharePoint how ever doesn’t have these cool metadata.xml files, so configuring that, will take a tiny bit more work, but not to worry, that is why we got PowerShell.

  • Open a PowerShell prompt locally on any of the servers in your farm. And run the following PowerShell Script . ( there is a link on the Relaying Party page too ) Make sure you modify the variables at the top to fit your installation
  • Head to the STS, click management, type a meaningful name into “Create new relying part” field, and press Create
    If you went with the defaults in the script, SharePoint should be sending a reply URL, so we can leave PassiveRequestorURI blank.
    SharePoint (default) doesn’t want the claims to be encrypted so we don’t need to add a certificate.
    Under “Audience Uris” add each of the URN’s that was mapped for each of the websites, show in the end of the script.

And you are all done.

Adding Azure AD, office 365, Azure ACS or an ADFS server
That’s all nice, but what about all your local users, or all your Office 365 users ?
Not a problem, head to the “Providers” page, and type in the URL to the metadata file.

If you are using Office 365 or Azure AD the URL will look something like this
https://accounts.accesscontrol.windows.net/<tenant>.onmicrosoft.com/FederationMetadata/2007-06/FederationMetadata.xml

If your using Microsoft ACS, your URL will look something like this
https://<yourname>.accesscontrol.windows.net/FederationMetadata/2007-06/FederationMetadata.xml
(note, someone at Microsoft fucked this up, so it doesn’t sent a Signing Certificate, but it does doing Office 365 … it’s the same certificate thou, so is easy to grab )

If your using ADFS, the URL will look something like
https://adfs.wingu.dk/FederationMetadata/2007-06/federationmetadata.xml

If your using ACS or ADFS, you don’t need to do anything else than going to the ADFS Server/Azure ACS/Azure AD website and add https://STS/FederationMetadata.xml and it will all configure it self. If how ever your using Office 365, you need to add a ServicePrincipal representing the STS. At the top of the Provider screen is a link to a powershell scrip that will set this up. Once the script is run, past in the “spn:<guid>” from the output of the script into the Target Realm field of the new Provider.

Lastly, go back to your Relying Party, and you can now also select your new Office 365 or what ever Provider you added, and let your users sign in using that

SharePoint 2010 and claims based authentication made a little bit easier

updated 05/03-2013: Added support for extracting root cert from signing certificate
So you have ADFS 2.0 installed. Or your reading some guide about how to configure SharePoint to allow login in using Office 365 or Azure AD or any other kind of Secure Token Service. And 99,9% of them will tell you to copy 1 or more certificates, and run a lot of weird PowerShell command, and it all ends up with you being able to setup some awesome new login page here

image

Well, fear not. If your STS support generating a FederationMetadata.xml URL, you can now use this PowerShell Script to quickly setup your SPTrustedIdentityTokenIssuer . The script can also setup Uri mappings and configure the websites, but I do not recommend using that part in other than development/test environments. Make sure to modify the variables at the top.

I’ve updated the script. If your STS was using a self issued certificate, or a certificate from an internal CA, it wouldn’t be trusted by SharePoint, so added a bit of code to traverse the certificate chain, and then add the root certificate to the Trusted store and also add the root to the list of trusted certificates inside SharePoint

# URL for ADFS / ACS / Office 365 / SimpleSTS / Wingu Cloud ControlPanel / what ever

# $metadataurl = 'https://adfs.wingu.dk/FederationMetadata/2007-06/federationmetadata.xml' # ADFS
# $metadataurl = 'https://accounts.accesscontrol.windows.net/skadefro.onmicrosoft.com/FederationMetadata/2007-06/FederationMetadata.xml' # Azure AD
# $metadataurl = 'https://skadefro.accesscontrol.windows.net/FederationMetadata/2007-06/FederationMetadata.xml' # Azure ACS - Wont work, apperently the guy who made this, forgot to add the signing certificat .. :-(
# $metadataurl = 'https://simplests.wingu.dk/FederationMetadata.xml' # Simple STS
# $metadataurl = 'http://admin.wingu.dk/sts2/FederationMetadata/2007-06/federationmetadata.xml' # Wingu Cloud Control Panel
$metadataurl = 'https://simplests.wingu.dk/FederationMetadata.xml'


# This should really be NameID, but often people will use emails or UPN. choose what fits your needs
$IdentifierClaim = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"
# What claims do you want mapped. All will be exposed in the ClaimsToken, but what will show up in the people picker ?
$mapClaims = @('UPN', 'Role')

# Default realm. If you only have 1 website, you can add that here, but more correctly would be adding
# Each URI accessible on your farm, mapping it to a URN
# If your using Wingu Cloud Control Panal or SimpleSTS, urn:sps will automaticly be converte to a SharePoint signin URI
# urn:sps:http://spfarm01.cloudapp.net becomes http://spfarm01.cloudapp.net/_trust/
# if not, just use the full uri, like http://spfarm01.cloudapp.net/_trust/
# if you want several sharepoint sites to allow the user access without prompting for login each time,
# use one shared urn (not URI) for all those sites. There are certain restrictions, on how/where you can do that
$defaultrealm = 'urn:sps:https://sharepoint.domain.com'

# Be carefull with this. Not all STS' understand/support's this, and often the above is a simpler solution
# set this to true, will go though the list of sites in the farm and add an "urn:sps:URI" entry for each site
$autoGenerateProviderRealms = $false

# Dont, Dont, DONT ... D-O-N-T, use this
# This will add Windows and ALL identityProviders to all WebApplication's (except Central Admin) for the "default" zone.
$autoAddSTS = $false

$stsname = 'SimpleSTS'
$name = $stsname
$realm = $null
$stsd = $null
$asd = $null

$snapin = Get-PSSnapin | where {$_.name -eq 'Microsoft.SharePoint.PowerShell'}
if($snapin -eq $null){ Add-PSSnapin Microsoft.SharePoint.PowerShell }


function Get-medadata ($metadataurl) {
[void] [Reflection.Assembly]::LoadWithPartialName("Microsoft.IdentityModel")
$STSReader = New-Object Microsoft.IdentityModel.Protocols.WSFederation.Metadata.MetadataSerializer
$req = [System.Net.WebRequest]::Create($metadataurl)
$xmlreader = [System.Xml.XmlReader]::Create($req.GetResponse().GetResponseStream())
$metadata = $STSReader.ReadMetadata([System.Xml.XmlReader]$xmlreader)
$metadata
}

function Get-STS ($metadata) {
foreach($RoleDescriptor in $metadata.RoleDescriptors){
if($RoleDescriptor.GetType().FullName -eq 'Microsoft.IdentityModel.Protocols.WSFederation.Metadata.SecurityTokenServiceDescriptor'){
# metadata have a STS
$stsd = $RoleDescriptor
} elseif($RoleDescriptor.GetType().FullName -eq 'Microsoft.IdentityModel.Protocols.WSFederation.Metadata.ApplicationServiceDescriptor'){
# metadata have a Relying Party
$asd = $RoleDescriptor
}
}
$stsd
}
function Get-RelyingParty ($metadata) {
foreach($RoleDescriptor in $metadata.RoleDescriptors){
if($RoleDescriptor.GetType().FullName -eq 'Microsoft.IdentityModel.Protocols.WSFederation.Metadata.SecurityTokenServiceDescriptor'){
# metadata have a STS
$stsd = $RoleDescriptor
} elseif($RoleDescriptor.GetType().FullName -eq 'Microsoft.IdentityModel.Protocols.WSFederation.Metadata.ApplicationServiceDescriptor'){
# metadata have a Relying Party
$asd = $RoleDescriptor
}
}
$asd
}

function Get-GetCertificate ($stsd, $certificatetype) {
if( ($certificatetype -ne 'Signing') -and ($certificatetype -ne 'Encryption')){
Write-Error "CertificateType should be 'Signing' or 'Encryption'"
return
}
$has40 = [Reflection.Assembly]::LoadWithPartialName("System.IdentityModel")

$SigningCertificate = $null
foreach($key in $stsd.Keys){
if($key.Use -eq $certificatetype){
if($has40.GetName().Version.Major -eq 4){
#if .net 4.0 is install we can just do this, when System.IdentityModel v4.0.30319 is loaded
$bytes = $key.Keyinfo.GetX509RawData()
} else {
#but default, sharepoint 2010 only have version v2 loaded so, lets do it, the ugly way
$str = $key.Keyinfo.ToString()
$str = $str.Substring( $str.IndexOf('RawData = ') + 10)
$str = $str.Substring(0, $str.IndexOf(')'))

$bytes = ([system.Text.Encoding]::UTF8).GetBytes($str)
}
$SigningCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]([byte[]]$bytes)
}
}
$SigningCertificate
}

Function Ensure-SPProvider($name, $defaultrealm, $SigningCertificate, $SigninURL, $claims, $IdentifierClaim, $mapClaims){
$certificatename = "$($name) Signing Certificate"
$cert = get-SPTrustedRootAuthority | ?{($_.Certificate.Thumbprint -eq $SigningCertificate.Thumbprint) -or ($_.Name -eq $certificatename)}
if(!$cert){
Write-Host "Adding $($SigningCertificate.Thumbprint) as a Trust Certificate"
$cert = New-SPTrustedRootAuthority -name $certificatename -Certificate $SigningCertificate
} else {
$cert | Set-SPTrustedRootAuthority -Certificate $SigningCertificate
}
$chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
$chain.Build($SigningCertificate)
$SigningCertificateRoot = $chain.ChainElements[$chain.ChainElements.Count -1].Certificate

# Ensure Root Cert has been added to Trusted Store
$store = get-item cert:\LocalMachine\Root
$store.Open("ReadWrite")
$store.Add($SigningCertificateRoot )
$store.Close()

# And now, add it to SharePoint too
$rootcert = get-SPTrustedRootAuthority | ?{($_.Certificate.Thumbprint -eq $SigningCertificateRoot.Thumbprint)}
if(!$rootcert){
Write-Host "Adding $($SigningCertificateRoot.Thumbprint) as a Trust Certificate"
$rootcert = New-SPTrustedRootAuthority -name $SigningCertificateRoot.Subject -Certificate $SigningCertificateRoot
}

$IDClaim = $null
$claimsmappings = @()
foreach($claim in $claims) {
# http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name is reserved inside SharePoint (??? Why ?... really, WTF !!!!)
# http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid is only used in SharePoint 2013 ( not tested yet )
# its SharePoints internal STS job to handle the requiered fields
# http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod
# http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant

if($claim.ClaimType -eq 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'){
$claimsmappings += New-SPClaimTypeMapping $claim.ClaimType -IncomingClaimTypeDisplayName $claim.DisplayTag -LocalClaimType 'http://simplests.wingu.dk/identity/claims/name'
} elseif($claim.ClaimType -eq 'http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod'){
# Skip
} elseif($claim.ClaimType -eq 'http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant'){
# Skip
} elseif($claim.ClaimType -eq 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'){
$claimsmappings += New-SPClaimTypeMapping $claim.ClaimType -IncomingClaimTypeDisplayName $claim.DisplayTag -LocalClaimType 'http://simplests.wingu.dk/identity/claims/nameidentifier'
} else {
$claimsmappings += New-SPClaimTypeMapping -IncomingClaimType $claim.ClaimType -IncomingClaimTypeDisplayName $claim.DisplayTag -SameAsIncoming
}
if($IdentifierClaim -eq $claim.ClaimType){
$IDClaim = $claim.ClaimType
}
}

$ap = Get-SPTrustedIdentityTokenIssuer | ?{$_.name -eq $name}
if(!$ap){
$ap = New-SPTrustedIdentityTokenIssuer -Name $name -Description $name -Realm $defaultrealm -ImportTrustCertificate $SigningCertificate -ClaimsMappings $claimsmappings -SignInUrl $SigninURL -IdentifierClaim $IDClaim
}
# Ensure all claim mappins are there
#foreach($claim in $claims) {
# $hasClaim = $ap.ClaimTypes | ?{$_ -eq $claim.ClaimType}
# if(!$hasClaim) {
# Write-Host "adding ClaimType $($claim.DisplayTag) to $name"
# $ap.ClaimTypes.Add($claim.ClaimType)
# }
#}

#$ap.Update()
#Add-SPClaimTypeMapping -Identity $claim -TrustedIdentityTokenIssuer $ap
#$ap = Get-SPTrustedIdentityTokenIssuer | ?{$_.name -eq $name}

# if you only have 1 sharepoint site, and dont want to "bother" setting up
# Reply URL on your STS ( adfs - AD FS 2.0 Management -> Trust Releationships ->
# Relying Party Trusts -> Properties on RP Go to "Endpoints" tab -> WS-Federation Passive Endpoint
# Or SimpleSTS -> Management -> Relying Party -> Manage -> PassiveRequestorURI
# you can just override the $ap.DefaultProviderRealm
# but the correct way would be to list each unique URI with an associated URN in $ap.ProviderRealms
$ap.DefaultProviderRealm = $defaultrealm

$ap.ProviderUri = $SigninURL
#$ap.Name = $name
#$ap.DisplayName = $name


# If WReply is not set, sharepoint will not add return URL when sending user to login page
# if DefaultProviderRealm/$ap.ProviderRealms URN is not listed with a Return URL in the STS
# the STS will not know where to send the user. What's "correct" ? dont know.
# if your paranoid, leacing WReply $false, using URN's all the places, and keeping Passive Federation
# URL's in you STS would proberly be best, but let's ensure we can have both unique URI's, prober return URI's
# and unique'nes by using $ap.ProviderRealms
$ap.UseWReplyParameter = $true

# HomeRealm tells the STS how the user want to to login instead of prompting.
# ADFS will ask and save a cookie, for 3 months, most custom solutions, will prompt each time
# for instance SimpleSTS will allow you to choose each time from a list of all allowed providers
# Sharepoint will NEVER know what those homerealms could be.
# SharePoint's internet STS will normaly "strip" off HomeRealm ( WHR= ) but with the Juni RU this new
# parameter was added to allow adding HomeRealm in custom code (modules or custom login page) without loosing the WHR parameter
# http://blogs.msdn.com/b/chriskeyser/archive/2011/10/02/sharepoint-2010-claims-and-home-realm-discover-passing-whr-on-the-url-to-sharepoint.aspx
try {
$ap.UseWHomeRealmParameter = $true
} catch {
Write-Warning "SharePoint is not update, and wont allow setting UseWHomeRealmParameter, make sure you use uri:sp: as realms/AudienceURIs, or set PassiveRequestorURI to [SharePointURI]/_trust/"
$Error.Clear()
}
$ap.Update()

}
$metadata = Get-medadata $metadataurl
$stsrealm = $metadata.EntityId.Id
$stsd = Get-STS $metadata
#$asd = Get-RelyingParty $metadata
$SigningCertificate = Get-GetCertificate $stsd 'Signing'
#$encryptionCertificate = Get-GetCertificate $asd 'Encryption'
$claims = $stsd.ClaimTypesOffered

foreach($PassiveRequestorEndpoint in $stsd.PassiveRequestorEndpoints){
$SigninURL = $PassiveRequestorEndpoint.Uri.AbsoluteUri
}

Ensure-SPProvider $stsname $defaultrealm $SigningCertificate $SigninURL $claims $IdentifierClaim $mapClaims

if($autoGenerateProviderRealms -eq $true){
# We could use Get-SPWebApplication, and then Get-SPAlternateURL on each site
# we could also filter out only "external" sites, etc ....

$ap = Get-SPTrustedIdentityTokenIssuer | ?{$_.name -eq $stsname}
#$ap.ProviderRealms.Clear()
foreach($wa in Get-SPWebApplication){
$pr = $ap.ProviderRealms.keys | where {$_.AbsoluteUri -eq $wa.Url}
if(!$pr){ $pr = $ap.ProviderRealms.Add([uri]($wa.Url), "urn:sps:$($wa.Url)") }
}
$ap.Update()
}
if($autoAddSTS -eq $true){
$windowsap = New-SPAuthenticationProvider
#$aps = Get-SPAuthenticationProvider -Zone "Default" -WebApplication $wa
$aps = @()
$aps += $windowsap
foreach($ap in Get-SPTrustedIdentityTokenIssuer){
$aps += $ap
}
foreach($wa in Get-SPWebApplication){
Set-SPWebApplication -Identity $wa -Zone "Default" -AuthenticationProvider $aps # -Force
}
}

$ap = Get-SPTrustedIdentityTokenIssuer | ?{$_.name -eq $stsname}
Write-Host "ensure '$($defaultrealm) and each 'Value' is added to the Relying Party in the STS"
$ap.ProviderRealms

onsdag den 6. februar 2013

Requesting email when using Windows LiveID OAuth2

didn’t take long to figure out, but just in case. If you are using DotNetOpenAuth and are using the ApplicationBlock.WindowsLiveGraph to get the identity of the user, you will not get the email address of the user. You can how ever request it, by sending wl.emails in the Scope:

Dim WindowsLiveIDTokenManager As New SQLTokenManager(WindowsLiveIDConsumerKey, WindowsLiveIDConsumerSecret)
Dim WindowsLiveClient As New DotNetOpenAuth.ApplicationBlock.WindowsLiveClient() With { _
    .ClientIdentifier = WindowsLiveIDConsumerKey, _
    .ClientSecret = WindowsLiveIDConsumerSecret _
}
Dim authUrl = New Uri(Request.Url.Scheme + "://" + Request.Url.Authority + "/sts2/login.aspx")
WindowsLiveClient.RequestUserAuthorization(scope:=New String() {ApplicationBlock.WindowsLiveClient.Scopes.Basic, "wl.emails"}, returnTo:=authUrl)

Next, when validating the access token, don’t use the built in JSON Deserialize function, but use your own

 

If authorization IsNot Nothing Then
    Dim request = System.Net.WebRequest.Create("https://apis.live.net/v5.0/me?access_token=" + Uri.EscapeDataString(authorization.AccessToken))
    Using response = request.GetResponse()
        Dim JSON As String = ""
        Using responseStream = response.GetResponseStream()
            Dim reader As New IO.StreamReader(responseStream, Encoding.UTF8)
            JSON = reader.ReadToEnd()
        End Using

        Dim jss As New Script.Serialization.JavaScriptSerializer
        Dim LiveIDUser As LiveIDGraph = jss.Deserialize(JSON, GetType(LiveIDGraph))    End Using
End If

 

And this is how your LiveIDGraph would look like

Imports System.Runtime.Serialization

<DataContract(Namespace:="")>
Public Class LiveIDGraph

    <DataMember> Public id As String
    <DataMember> Public name As String
    <DataMember> Public first_name As String
    <DataMember> Public last_name As String
    <DataMember> Public link As String
    <DataMember> Public gender As String
    <DataMember> Public emails As LiveIDEmails
    <DataMember> Public locale As String
    <DataMember> Public updated_time As DateTime

End Class

<DataContract(Namespace:="")>
Public Class LiveIDEmails
    <DataMember> Public preferred As String
    <DataMember> Public account As String
    <DataMember> Public personal As String
    <DataMember> Public business As String
End Class

tirsdag den 5. februar 2013

Fully automated deployment of SharePoint farms in Microsoft Azure

There a plenty of blogs out there about scripting SharePoint, so I wont fill up more space about all the troubles and joys regarding that. I will how ever point you in the direction of AutoSpInstaller who have an awesome script for slip streaming SharePoint installations and better yet. Automating the deployment of SharePoint 2010 and 2013.

I’ve seen a few blog post about installing SharePoint in Microsoft Azure, and I wanted to add my two cent about that. I just finished up doing a Proof of Concept showing a FULLY automated installation of multiple both single and multiple server, SharePoint farm, inside Microsoft Azure. This is how I did it.

I’m developing a Provisioning Engine that supports running in many sites and distribute servers and services across these sites seamlessly. I order to start facilitating Azure we need a subscription and some Persistent VMs deployed inside that subscription. I originally wanted to do everything “offsite” from each subscription, so a new customer could start from scratch, but after a while it became clear I needed some more granulated control doing deployment and needed to deploy my provisioning engine inside the subscription. Microsoft does not have an API for creating subscriptions (that I know off) so we manually sign up for a new Azure Subscription. Next we sign up for a Trial Account in our Provisioning Site. Create a certificate and upload it to Azure and Add Subscription ID and certificate to my Provisioning API in order to make the provisioning engine able to facilitate Azure for that subscription.

We do that by adding a Provisioning Site,

image

Next, we need to make sure all servers can see each other. We can install everything on one server. We can add all servers to the same Azure Hosted Service. We can “link” different roles to a Azure Hosted Service (I have not tested this) or we can create a Virtual Network. The latter is most appealing, so we add a Workspace

image

( at the time of writing, I have not automated the creating of the Virtual Network inside Azure, so we need to add it manually inside Azure and fill out the values in the Workspace plan )

image

Once this is done, manually Add a server to Azure, and deploy our provisioning engine. (this could later be a custom image, or pushed from another site, but for proof of concept, doing this manually  is ok). We link the server to the provisioning site, so the new server have access to all services, and we are now ready to automated everything else.

Next we add an Microsoft SQL server. This can also be done while adding the SharePoint Farm
image

Let customize the SharePoint installation so we control Server names etc. Create a Hosted Service for our new SharePoint Farm

image

image

Next add 1 or more servers depending on how big you want the farm to be

image

Make sure to update relation to point to your new Azure Hosted Service if you got more than one. Lastly we need to add a SharePoint farm on top of the Hosted Service.
image
Fill out desired SMTP/Email settings if needed, and hit deploy. This will automatically add a new Hosted Service and add an SQL Server inside that Service. It will add a Plan for each of the (up to) 22 (default is 16) databases  needed to run the farm, once all have been setup it will deploy SharePoint to each of the servers you added in the above steps, it will create a new Farm, a default SharePoint site and My Site. Configure most of the normal features of a SharePoint farm ( but any features supported by AutoSpInstaller is supported here too)

So what happens behind the scene? Well, AutoSpInstaller supports deploying SharePoint remotely on one or more servers, but it uses PS Exec from PS Tools to do it. ( if you look in the PowerShell scripts it looks like the author also tried making it work using WSMan ( invoke-computer / new-pssession )) but a new blank server doesn’t allow connect with either, so we need a way to get in contact with the server in the first place. If you deploy the standard Windows 2003 image, it allows you to talk with it, using WMI but not WSMan, so you could fire off a WMI command to run the command to configure WSMan ( winrm quickconfig ) but if you deploy a Windows 2012 server, your just out of luck, neither WMI or WSMan will work. You can how ever, tell Windows Azure to automatically join new servers to a domain, and when we are talking about SharePoint you need the servers to be domain joined. Once a Windows server 2012 is part of a domain, you can connect using WSMan and execute PowerShell on the machine (but not WMI, hence PS Exec will not work )

Sure, you could just fire up all servers to be part of a domain, create a group policy to configure the firewall (and maybe there is a Policy for doing something like a winrm quickconfig  too?) but I wanted something generic. Granted, I never figured out a way to “cheat” a fresh Windows 2012 that is not a domain member, I guess I will need to create a custom Image for those later, but for now, having them domain joined was good enough.

On all servers I deploy, I automatically I push out a small windows service. It handles certificates, joining/disjoining domains, installing software, remote management and is also used on Remote Desktop and Citrix XenApp servers to manage user sessions. Its perfect for handling the SharePoint installation and with above I can now push out this service though WMI and/or WSMan and kick of the installation. So the service handles installing an SQL server. well tested and Known technology and easy to automate, next we can now kick off the SharePoint installation.

AutoSpInstaller uses Start-Transcript doing installation, and I haven't figured out how to “support” that in a custom hosted PowerShell host, so first I had to strip that out. Next it does an insane amount of “am waiting lets do a write a “.” dot each second” thing, and is annoying as hell in the console logs, so had to strip those out too. Lastly you need patience … A LOT of it … So now when looking at the Provisioning Request Log is more clean.

imageimage

For some reason the first server you install, that will also handle setting up the farm, and configuring services, will take from 2½ to 4 hours depending on the amount of services you choose. The following servers can take up to 30 minutes to install binaries, and 20 to 40 minutes joining the farm and configuring services. Not sure why really (both CPU/memory and Disk queue length is way below any warning signs on all servers doing the installation)

Once AutoSpInstaller is done, its easy and well documented how to connect to one of the servers and configuring new SharePoint sites/Site Collections and adding/removing users and groups, as part of my provisioning engine.

When using AutoSpInstaller it worth taking 2 minute extra reading the comments in the AutoSPInstallerInput.xml file. Stuff like how database prefix's, limits on characters' in password, or knowing that what worked on your “single” server install will not work in farms ( using the default http://localhost as WebApp name’s or not limiting the QueryServer to a single server will take a lot longer to troubleshoot that just reading the damn documentation Winking smile )