lørdag den 23. februar 2013

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


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)

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

function Get-GetCertificate ($stsd, $certificatetype) {
if( ($certificatetype -ne 'Signing') -and ($certificatetype -ne 'Encryption')){
Write-Error "CertificateType should be 'Signing' or 'Encryption'"
$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)

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)}
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
$SigningCertificateRoot = $chain.ChainElements[$chain.ChainElements.Count -1].Certificate

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

# And now, add it to SharePoint too
$rootcert = get-SPTrustedRootAuthority | ?{($_.Certificate.Thumbprint -eq $SigningCertificateRoot.Thumbprint)}
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}
$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)
# }

#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/"

$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}
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)") }
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"

Ingen kommentarer:

Send en kommentar