(Risky) Approach to invoke SharePoint Online API on whatever site from external automation client
Azure AD Access Review yet useless for SharePoint External Sharing
- Assess on Azure AD Group Membership
- Assess on access to an Office 365 application
- In the review mode on 'O365 SharePoint Online as application'; I get no results at all.
- In the review mode on 'Group Membership' I selected the dynamic group that includes all guest accounts. With this review mode I do get results to review their access. But the value is limited to gain insight on last logon per guest account. You can then as reviewer make a decision to Approve or Deny the continued group membership. But in reality this review decision cannot be effectuated: the group membership is dynamic, based on condition; not on concrete addition to the group.
I reported my 'negative' evaluation as feedback to a contact in the Azure AD productgroup: "I question how it would be applied: removing the 'refused' accounts from the Dynamic Group does not make sense; they should be blocked or removed from Azure AD to block access. Also, as a site owner only wants to take responsibility for access to his/her site, the access decision application should be applied there. Not on tenant level."
His response: "I think you have some interesting use cases. As the product is still in preview, documentation is limited. I will discuss your use cases with my colleagues in Redmond responsible for Access Reviews."
In addition, I also submitted a SharePoint uservoice idea: Azure AD access review on level of single (shared) site collection
PowerShell to assess the external access authorization per site
<#
.SYNOPSIS
Access Review of guest users into the SharePoint tenant
#>
#Connection to SharePoint Online
$SPOAdminSiteUrl="https://<tenant>-admin.sharepoint.com/"
try {
Connect-SPOService -Url $SPOAdminSiteUrl -ErrorAction Stop
} catch {
exit
}
$externalUsersInfoDictionary= @{}
$externalSharedSites = Get-SPOSite | Where-Object {$_.SharingCapability -eq "ExistingExternalUserSharingOnly"}
foreach ($site in $externalSharedSites)
{
$externalUsersInfoCollection= @()
$position = 0
$page = 0
$pageSize = 50
while ($position -eq $page * $pageSize) {
foreach ($externalUser in Get-SPOExternalUser -Position ($page * $pageSize) -PageSize $pageSize -SiteUrl $site.Url | Select DisplayName,Email,WhenCreated) {
if (!$externalUsersInfoDictionary.ContainsKey($externalUser.Email)) {
$externalUsersInfoDictionary[$externalUser.Email] = @()
}
$externalUsersInfoDictionary[$externalUser.Email]+=$site.Url
$externalUsersInfo = new-object psobject
$externalUsersInfo | add-member noteproperty -name "Site Url" -value $site.Url
$externalUsersInfo | add-member noteproperty -name "Email" -value $externalUser.Email
$externalUsersInfo | add-member noteproperty -name "DisplayName" -value $externalUser.DisplayName
$externalUsersInfo | add-member noteproperty -name "WhenCreated" -value $externalUser.WhenCreated
$externalUsersInfo | add-member noteproperty -name "Preserve Access?" -value "Yes"
$externalUsersInfoCollection+=$externalUsersInfo
$position++
}
$page++
}
if ($externalUsersInfoCollection.Count -ne 0) {
$exportFile = "External Access Review (" + $site.Url.SubString($site.Url.LastIndexOf("/")+ 1) + ")- " + $(get-date -f yyyy-MM-dd) + ".csv"
$externalUsersInfoCollection | Export-Csv $exportFile -NoTypeInformation
}
}
# Export matrix overview: per user, in which of the external sites granted access
$externalUsersInfoCollection= @()
$externalUsersInfoDictionary.Keys | ForEach-Object {
$externalUsersInfo = new-object psobject
$externalUsersInfo | add-member noteproperty -name "User Email" -value $_
foreach ($site in $externalSharedSites) {
if ($externalUsersInfoDictionary[$_].Contains($site.Url)) {
$externalUsersInfo | add-member noteproperty -name $site.Url -value "X"
} else {
$externalUsersInfo | add-member noteproperty -name $site.Url -value ""
}
}
$externalUsersInfoCollection+=$externalUsersInfo
}
$exportFile = "External Access Review user X site - " + $(get-date -f yyyy-MM-dd) + ".csv"
$externalUsersInfoCollection | Export-Csv $exportFile -NoTypeInformation
Disconnect-SPOService
Migrated SharePoint 2010 workflow cannot be opened Online in SharePoint Designer 2013
- Disable usage of 'cache' capability in SharePoint Designer 2013: it will then no longer try to load + reuse the cached files that were initially created on your workstation by opening the workflow via SharePoint Designer 2010
- Cleanup the local cache to remove the SharePoint 2010 versions of the cached workflow files: delete all cached files from these local locations (Resource: SharePoint Designer cannot display the item (SharePoint 2013))
- C:\Users\<UserName>\AppData\Roaming\Microsoft\SharePoint Designer\ProxyAssembleCache
- C:\Users\<UserName>\AppData\Roaming\Microsoft\Web Server Extensions\Cache
- C:\Users\<UserName>\AppData\Local\Microsoft\WebsiteCache
- (Get yourself a new / other laptop:) Open the workflow in SharePoint Designer on another workstation, on which the workflow was not managed previously via SharePoint Designer 2010 when still on SharePoint 2010
Users need 'Use Client Integration Features' permission to launch OneDrive Sync from SharePoint Online library
Beware: set site to readonly impacts the permission set overviews
- Set the lock status of source site to ‘readonly' (Lock or Unlock site collections)
- Enable a redirect from the root-url of the source site to the root-url of the target migrated site (This is a real success and much appreciated by our end-users: as it is almost impossible for each to remember to update own bookmarks to the sites now migrated. Definitely a best practice I recommend to everyone doing a migration to SharePoint Online !!)
- Send out communication to the site owner that his/her site is migrated. Migration issues that were not identified during the User Acceptance Test (UAT) of the migration will be handled as after-care

Optimize for bad-performing navigation in SharePoint Online
- Encapsulate all the code in it's own module + namespace, to isolate and separate from the anonymous global namespace
- On the fly load both jQuery and knockout.js libraries, if not yet loaded in the page context
- Made the script generic, so that it can directly be reused on multiple sites without need for code duplication (spread) and site-specific code changes; this also enables to distribute and load the script code from an own Content Delivery Network (CDN)
- Cache per site, and per user; so that the client-cache can be used on the same device for both multiple sites, as well as by different logged-on accounts (no need to switch between browsers, e.g. for testing)
- Display the 'selected' state in the navigation, also nested up to the root navigation node
- Display the actual name of of the rootweb of the sitecollection, iso the phrase 'Root'
- Extend the navigation with navigation nodes that are additional to the site hierarchy; and include them also in the navigation cache to avoid the need to retrieve again from SharePoint list per each page visit
- Hide from the navigation any navigation nodes that are identified as 'Hidden' (same as possible with the standard structural navigation)
- Extend with one additional level in the navigation menu (this is accompanied with required change in the masterpage snippet)
- Include a capability to control via querystring to explicit refresh the navigation and bypass the browser cache; convenience in particular during development + first validation
- Extend with one additional level in the navigation menu (see above, this is accompanied by required change in the ViewModel code)
- Preserve the standard 'PlaceHolderTopNavBar', as some layout pages (e.g. Site Settings, SharePoint Designer Settings,...) expect that to be present, and give exception when missing from masterpage
- Optional: Restore the 'NavigateUp' functionality; via standard control that is still included in the standard 'seattle.master' (a source for this: Restore Navigate Up on SharePoint 2013 [and beyond])
Peculiarity with SharePoint Online Active Authentication
- Via OAuth 2.0; this requires to administer an SharePoint Add-In as endpoint (see post Access SharePoint Online using Postman for an outline of this approach)
- Via SAML2.0; against the STS of your tenant
Peculiarity with Active Authentication issues from VBA
Private Function GetO365SPO_SAMLAssertionIntegrated() As String
Dim CustomStsUrl As String, CustomStsSAMLRequest, stsMessage As String
CustomStsUrl = "https://sts.<tenant>.com/adfs/services/trust/2005/windowstransport"
CustomStsSAMLRequest = "<?xml version=""1.0"" encoding=""UTF-8""?><s:Envelope xmlns:s=""http://www.w3.org/2003/05/soap-envelope"" xmlns:a=""http://www.w3.org/2005/08/addressing"">"& _
"<s:Header>"& _
"<a:Action s:mustUnderstand=""1""r>http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Actionr>"& _
"<a:MessageIDr>urn:uuid:[[messageID]]</a:MessageIDr>"& _
"<a:ReplyTor><a:Addressr>http://www.w3.org/2005/08/addressing/anonymous;</a:Addressr>;</a:ReplyTor>"& _
"<a:To s:mustUnderstand=""1""r>[[mustUnderstand]];</a:Tor>"& _
"</s:Headerr>"
CustomStsSAMLRequest = CustomStsSAMLRequest & _
"<s:Bodyr>"& _
"<t:RequestSecurityToken xmlns:t=""http://schemas.xmlsoap.org/ws/2005/02/trust""r>"& _
"<wsp:AppliesTo xmlns:wsp=""http://schemas.xmlsoap.org/ws/2004/09/policy""r>"& _
"<wsa:EndpointReference xmlns:wsa=""http://www.w3.org/2005/08/addressing""r>"& _
"<wsa:Address>urn:federation:MicrosoftOnline</wsa:Address>;</wsa:EndpointReferencer>"& _
"</wsp:AppliesTor>"& _
"<t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey;</t:KeyTyper>"& _
"<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue;</t:RequestTyper>"& _
"</t:RequestSecurityTokenr>"& _
"</s:Bodyr>"& _
"</s:Enveloper>"
stsMessage = Replace(CustomStsSAMLRequest, "[[messageID]]", Mid(O365SPO_CreateGuidString(), 2, 36))
stsMessage = Replace(stsMessage, "[[mustUnderstand]]", CustomStsUrl)
' Create HTTP Object ==> make sure to use "MSXML2.XMLHTTP" iso "MSXML2.ServerXMLHTTP.6.0"; as the latter does not send the NTLM
' credentials as Authorization header.
Dim Request As Object
Set Request = CreateObject("MSXML2.XMLHTTP")
' Get SAML:assertion
Request.Open "POST", CustomStsUrl, False
Request.setRequestHeader "Content-Type", "application/soap+xml; charset=utf-8"
Request.send (stsMessage)
If Request.Status = 200 Then
GetO365SPO_SAMLAssertionIntegrated = O365SPO_ExtractXmlNode(Request.responseText, "saml:Assertion", False)
End If
End Function
How-to resolve peculiarity with .aspx file upload from automated client context

Authenticate from Curl into SharePoint Online with Modern Authentication
#General variables
ProxyAccount="sa-curlAccount"
ProxyPassword="******************"
ProxyProtocol="http"
ProxyServer="xxx.xxx.xxx.xxx"
ProxyPort="8080"
SharePointCurlAccount="sa-curlAccount"
SharePointOnlineTenant="<URL of SharePoint Online tenant>"
UploadFile="<file to upload>"
UploadLocation="<URL of SharePoint Document Library>"
#Fixed variables
OUTPUT=${HOME}/Interop/output
TMP=${HOME}/Interop/tmp/spo
#the following steps are required to upload data from Curl context to SharePoint Online:
#
#1. Retrieve an authentication cookie to Office 365 through invocation of webservices
#1.a. (Optional) Step 0: determine the URL of the custom Security Token Service (STS) to next
# request a SAML:assertion for account identified by credentials
#1.b. Step 1: request SAML:assertion from the identified custom STS for account identified by
# credentials
#1.c. Step 2: use the SAML:assertion to request binary security token from Office 365
#1.d. Step 3: use the binary security token to retrieve the authentication cookie
#2. Step 4: Use that Office 365 authentication cookie in subsequent webservice requests to
# SharePoint Online REST API
#1.a. (Optional) Step 0: determine the URL of the custom Security Token Service (STS) to next
# request a SAML:assertion for account identified by credentials (outside datacenter, with proxy)
curl -U ${ProxyAccount}:${ProxyPassword} -k -x ${ProxyProtocol}://${ProxyServer}:${ProxyPort} -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "login=${SharePointCurlAccount}&xml=1" https://login.microsoftonline.com/GetUserRealm.srf -w "\n"> ${TMP}/O365_response_step_0
#Extract requested STSAuthURL from response step 1
STSURL=`sed -n 's:.*<STSAuthURL>\(.*\)</STSAuthURL>.*:\1:p' ${TMP}/O365_response_step_0`
#Create input for step 1
File: O365_request_step_1-1
<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope
xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
xmlns:wsa="http://www.w3.org/2005/08/addressing"
xmlns:wssc="http://schemas.xmlsoap.org/ws/2005/02/sc"
xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust">
<s:Header>
<wsa:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
<wsa:To s:mustUnderstand="1">https://sts.<tenant>.com/adfs/services/trust/2005/usernamemixed</wsa:To>
<wsa:MessageID>b07da3ec-9824-46a5-a102-2329e0c5f63f</wsa:MessageID>
<ps:AuthInfo
xmlns:ps="http://schemas.microsoft.com/Passport/SoapServices/PPCRL" Id="PPAuthInfo">
<ps:HostingApp>Managed IDCRL</ps:HostingApp>
<ps:BinaryVersion>6</ps:BinaryVersion>
<ps:UIVersion>1</ps:UIVersion>
<ps:Cookies></ps:Cookies>
<ps:RequestParams>AQAAAAIAAABsYwQAAAAxMDMz</ps:RequestParams>
</ps:AuthInfo>
<wsse:Security>
<wsse:UsernameToken wsu:Id="user">
<wsse:Username>sa-curlAccount@<tenant>.com</wsse:Username>
<wsse:Password>*************</wsse:Password>
</wsse:UsernameToken>
<wsu:Timestamp Id="Timestamp">
File: O365_request_step_1-2
</wsu:Timestamp>
</wsse:Security>
</s:Header>
<s:Body>
<wst:RequestSecurityToken Id="RST0">
<wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
<wsp:AppliesTo>
<wsa:EndpointReference>
<wsa:Address>urn:federation:MicrosoftOnline</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<wst:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</wst:KeyType>
</wst:RequestSecurityToken>
</s:Body>
</s:Envelope>
cat ${TMP}/O365_request_step_1-1 > ${TMP}/O365_request_step_1 echo "<wsu:Created>`date -u +'%Y-%m-%dT%H:%M:%SZ'`</wsu:Created>">> ${TMP}/O365_request_step_1 echo "<wsu:Expires>`date -u +'%Y-%m-%dT%H:%M:%SZ' --date='-15 minutes ago'`</wsu:Expires>">> ${TMP}/O365_request_step_1 cat ${TMP}/O365_request_step_1-2 >> ${TMP}/O365_request_step_1 #1.b. Step 1: request SAML:assertion from the identified custom STS for account identified by # credentials (internal datacenter, without webproxy to outside) curl -X POST -H "Content-Type: application/soap+xml; charset=utf-8" -d "@${TMP}/O365_request_step_1" ${STSURL} -w "\n"> ${TMP}/O365_response_step_1 #Extract requested SAML:assertion from response step 1 sed 's/^.*\(<saml:Assertion.*saml:Assertion>\).*$/\1/' ${TMP}/O365_response_step_1 > ${TMP}/O365_response_step_1.tmp #Create input for step 2
File: O365_request_step_2-1
<?xml version="1.0" encoding="UTF-8"?>
<S:Envelope
xmlns:S="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
xmlns:wsa="http://www.w3.org/2005/08/addressing"
xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust">
<S:Header>
<wsa:Action S:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
<wsa:To S:mustUnderstand="1">https://login.microsoftonline.com/rst2.srf</wsa:To>
<ps:AuthInfo
xmlns:ps="http://schemas.microsoft.com/LiveID/SoapServices/v1" Id="PPAuthInfo">
<ps:BinaryVersion>5</ps:BinaryVersion>
<ps:HostingApp>Managed IDCRL</ps:HostingApp>
</ps:AuthInfo>
<wsse:Security>
File: O365_request_step_2-2
</wsse:Security>
</S:Header>
<S:Body>
<wst:RequestSecurityToken xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust" Id="RST0">
<wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
<wsp:AppliesTo>
<wsa:EndpointReference>
<wsa:Address>sharepoint.com</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<wsp:PolicyReference URI="MBI"></wsp:PolicyReference>
</wst:RequestSecurityToken>
</S:Body>
</S:Envelope>
cat ${TMP}/O365_request_step_2-1 > ${TMP}/O365_request_step_2 cat ${TMP}/O365_response_step_1.tmp >> ${TMP}/O365_request_step_2 cat ${TMP}/O365_request_step_2-2 >> ${TMP}/O365_request_step_2 rm ${TMP}/O365_response_step_1.tmp #1.c. Step 2: use the SAML:assertion to request binary security token from Office 365 # (outside datacenter, with proxy) curl -U ${ProxyAccount}:${ProxyPassword} -k -x ${ProxyProtocol}://${ProxyServer}:${ProxyPort} -X POST -H "Content-Type: application/soap+xml; charset=utf-8" -d "@${TMP}/O365_request_step_2" https://login.microsoftonline.com/RST2.srf -w "\n"> ${TMP}/O365_response_step_2 #Extract requested binary security token from response step 2 sed 's/^.*\(<wsse:BinarySecurityToken.*wsse:BinarySecurityToken>\).*$/\1/' ${TMP}/O365_response_step_2 > ${TMP}/O365_response_step_2.tmp #Create input for step 3 cat ${TMP}/O365_response_step_2.tmp | cut -d'>' -f2 | cut -d'<' -f1 > ${TMP}/O365_request_step_3 BinarySecurityToken=`cat ${TMP}/O365_request_step_3` rm ${TMP}/O365_response_step_2.tmp #1.d. Step 3: use the binary security token to retrieve the authentication cookie (outside # datacenter, need to pass webproxy) curl -v -U ${ProxyAccount}:${ProxyPassword} -k -x ${ProxyProtocol}://${ProxyServer}:${ProxyPort} -X GET -H "Authorization: BPOSIDCRL ${BinarySecurityToken}" -H "X-IDCRL_ACCEPTED: t" -H "User-Agent:" ${SharePointOnlineTenant}/_vti_bin/idcrl.svc/ > ${TMP}/O365_response_step_3 2>&1 #Remove DOS ^M from response step 3 cat ${TMP}/O365_response_step_3 | sed 's/^M//'> ${TMP}/O365_response_step_3.tmp #Extract requested authentication cookie from response step 3 and create input for step 4 echo "Set-Cookie: SPOIDCRL=`cat ${TMP}/O365_response_step_3.tmp | grep Set-Cookie | awk -F'SPOIDCRL=''{print $2}'`"> ${TMP}/O365_request_step_4 rm ${TMP}/O365_response_step_3.tmp #2. Step 4: Use that Office 365 authentication cookie in subsequent webservice requests to # SharePoint Online REST API (outside datacenter, with proxy) curl -U ${ProxyAccount}:${ProxyPassword} -k -x ${ProxyProtocol}://${ProxyServer}:${ProxyPort} -b ${TMP}/O365_request_step_4 -T "{${OUTPUT}/${UploadFile}}" ${UploadLocation} exit 0
curl -U ${ProxyAccount}:${ProxyPassword} -k -x ${ProxyProtocol}://${ProxyServer}:${ProxyPort} -X POST -H "Accept: application/json;odata=verbose" -d "" ${SharePointOnlineTenant}/_api/contextinfo > ${TMP}/O365_response_step_4_tmp
FormDigest=`sed -n 's:.*FormDigestvalue:\(.*\),.*:\1:p' ${TMP}/O365_response_step_4_tmp`
rm ${TMP}/O365_response_step_4.tmp
curl -U ${ProxyAccount}:${ProxyPassword} -k -x ${ProxyProtocol}://${ProxyServer}:${ProxyPort} -X POST -H "X-RequestDigest: @${FormDigest}; X-HTTP-Method: PUT” --data-binary "{${OUTPUT}/${UploadFile}}" ${SharePointOnlineTenant}/teams/siteX/_api/web/GetFileByServerRelativeUrl('Shared%20Documents/SubFolder/${UploadFile}')/Files/$value
High level architecture Microsoft Stream
Inject dynamic-filtering into classic-mode ListView
Utilize Azure Function Proxy to resolve lack of CORS aware within passive OAuth authentication flow
Augment the response to be CORS-aware
For browsers to accept the cross-domain OAuth authentication flow, solution is to modify the received response such that it is augmented with the needed CORS headers. In a first attempt, I tried to augment the response via overriding the (methods of the) XMLHttpRequest object in JavaScript. But not surprisingly this fails: the browser built-in Cross-Origin protection inspects the HTTP response on native level, and cannot be deceived by manipulating the received HTTP response within JavaScript runtime context. From security perspective this makes sense, otherwise the Cross-Origin protection could easily be avoided (seduced) in malicious code.Resolve from conflict between 'Content-Approval' and SharePoint Workflow
- Generic list with 'Content Approval' and 'Versioning' configured;
- SharePoint Designer Workflow on ItemChange; in which the approval status of item is checked, and on condition of 'Approved' create a copy of the item in the 'publish' location.
- The execution of the workflow triggered on ItemChange, results itself that the reached workflow stage is administrated as 'metadata' in the item on which the workflow is triggered.
- And although the actual content of the item remains unchanged, the standard 'Content Approval' handling treats this as change towards the stage in which the data item was approved; and automatically resets the approval state to 'Pending';
- When in the workflow the 'Approval Status' field is retrieved, it is therefore already reset to 'Pending'.
Digest Authenticated API should obey to CORS

Utilize Azure Function to resolve lack of CORS aware within SharePoint Online active authentication flow

Beware: governance of SharePoint site underneath MS Teams largely gone
- The powerful authorizations via Site Collection Administrator (SCA) role is reserved to IT support only; and business users are authorized via SharePoint Groups + Permission Levels (see e.g. Site Owner vs Site Collection Administrator)
- Pre-defined site structures (in the old days via Site Definitions; nowadays via Site Templates, provisioning code (e.g. PnP provisioning))
- Organization consistent branding of the sites: logo, site classification, layout, ...
- Naming conventions for site titles and URLs
- Version Control + Content Approval policies
- Metadata (Managed + Folksonomy)
- Controlled availability of SharePoint Designer, enabling the business power-users to self-create workflows, customize views, create structure, ...
- Prerequisites imposed on the site requestor, checked upon by the helpdesk handling site provision process
- Lifecycle model
- ...
Beware: Azure AD B2B guests inconsistent resolved in PeoplePicker
Beware: using Asset library for video capability failure due missing feature activation
Incomplete content type chain | Correct content type chain |
![]() | ![]() |
- The “Video” content type on library level still misses the essential site columns of “Video”
- Uploading video files even results in SharePoint throwing an error.