Tag Archives: Office 365

A history of extensibility in SharePoint and how to choose a model

I recently wrote a post for my employer about the recent history of SharePoint extensibility models. It also touches on how we as company settled on the model with which we are currently delivering our Intranet/Digital-Workplace solution. I discuss the Feature Framework, Farm and Sandboxed Solutions, SharePoint Add-in Model, SharePoint Framework, Remote Provisioning, and more.

Check it out here: A history of extensibility in SharePoint and how Fresh chose a model

SharePoint extensibility model, Generic image

Office 365 CDN – Some Notes and Sample Scripts

The Office 365 CDN (Content Delivery Networks) may be activated to host SharePoint Online files in a more globally accessible manner. The general premise behind this is that static assets can be served to users from a location more local to them than the data centre in which the Office 365 tenant is located.

I won’t go into the real benefits of this beyond to say that my limited testing at this point leads me to believe that the performance impact of using a CDN will be negligible for the vast majority of users/organisations. This is because the volume of data which can be served via the CDN is not a significant proportion of the data impacting page load speed.

Regardless, the documentation around how to get started with the Office 365 CDN is decent. A good place to start is this link.

Private CDN with auto-rewrite
Private CDN with auto-rewrite. Image credit to Microsoft (https://dev.office.com/blogs/general-availability-of-office-365-cdn)

A couple of gotchas I’ve noticed

  • Fetching an image rendition using the width query string parameter does NOT correctly return the image rendition as configured. It simply scales the image to the specified width (i.e. no cropping or positioning is performed).
  • If all users are located in the same region as the Office 365 tenant, turning on the CDN may reduce performance due to CDN priming (replication of files to the CDN) and will complicate updates to files which are replicated (e.g. JavaScript in the Style Library).
  • Search web parts must be configured for ‘Loading Behaviour’ – ‘Sync option: Issue query from the server’ in order for the auto rewrite of CDN hosted files to occur. This is true for display templates as well as the value of the PublishingImage managed property

Office 365 CDN PowerShell Samples

I’ve got some sample PowerShell below showing how to activate the Office 365 CDN (there’s private and public, you can use either or both) and associate origins with it (an origin is a document library which will be replicated to the CDN).

I’ve also got a simple sample of how to remove all origins as there is not a single cmdlet for this. It is worth noting that although an enabled CDN with no origins is functionally identical to a disabled CDN (i.e. no files are being replicated) they are not the same from a configuration perspective.

Please note that these are just sample scripts and have not been parameterised as you may require.

Calling the PowerShell functions:

$cdnType = "Private" # Private or Public
$serverRelSiteCollectionUrl = "/sites/mysite" # site collection URL or * for all site collections

Authenticate-PowerShell

Set-CdnConfiguration $serverRelSiteCollectionUrl $cdnType

#Remove-CdnConfiguration $cdnType # This removes all origins but the CDN is still enabled
#Set-SPOTenantCdnEnabled -CdnType $cdnType -Enable $false # This disables the CDN

The PowerShell functions:

Function Authenticate-PowerShell() {
	[string]$tenantUrl = "https://TENANT-admin.sharepoint.com"
	[string]$adminUsername = "USER@TENANT.onmicrosoft.com"
	[string]$adminPassword = "PASSWORD"

	# Ensure module is loaded
	if ((Get-Module Microsoft.Online.SharePoint.PowerShell).Count -eq 0) {
		Import-Module Microsoft.Online.SharePoint.PowerShell -DisableNameChecking
	}

	$secureAdminPassword = $(convertto-securestring $adminPassword -asplaintext -force)
	$cred = New-Object -TypeName System.Management.Automation.PSCredential -argumentlist $adminUsername, $secureAdminPassword
	Connect-SPOService -Url $tenantUrl -credential $cred
}

Function Set-CdnConfiguration($serverRelSiteCollectionUrl, $cdnType){
    #LogWaiting "Configuring CDN"
    #LogInfo ""

    $fileTypes = "GIF,ICO,JPEG,JPG,JS,PNG,CSS,EOT,SCG,TTF,WOFF"
    $cdnOrigins = @(
        "$serverRelSiteCollectionUrl/_catalogs/masterpage", 
        "$serverRelSiteCollectionUrl/style library",  
        "$serverRelSiteCollectionUrl/sitecollectionimages",
        "$serverRelSiteCollectionUrl/publishingimages"#,
        #"$serverRelSiteCollectionUrl/news/publishingimages"
    )

    # Enable cdn WITHOUT default origins
    $supressOutput = Set-SPOTenantCdnEnabled -CdnType $cdnType -Enable $true -NoDefaultOrigins -Confirm:$false

    # Configure cdn origins (incl ensure default origins)
    Ensure-CdnOrigin $cdnType $cdnOrigins

    # Extend list of file types
    $supressOutput = Set-SPOTenantCdnPolicy -CdnType $cdnType -PolicyType IncludeFileExtensions -PolicyValue $fileTypes

    #LogSuccess "done"

    # Print status
    Get-SPOTenantCdnOrigins -CdnType $cdnType
}

Function Ensure-CdnOrigin($cdnType, $originUrls){
  $originUrls | ForEach {
    $oUrl = $_
    try {
      #LogWaiting "Adding origin: $oUrl"
      $supressOutput = Add-SPOTenantCdnOrigin -CdnType $cdnType -OriginUrl $oUrl -Confirm:$false
    }
    catch {
      if($Error[0].Exception.ToString().Contains("The library is already registered as a CDN origin")) {
        # aleady present, do nothing
      }
      else {
        #LogError $Error[0]
        Exit
      }
    }
    #LogSuccess "done"
  }
}

Function Remove-CdnConfiguration($cdnType){
	(Get-SPOTenantCdnOrigins -CdnType $cdnType)	| ForEach { $_ | ForEach { $supress = Remove-SPOTenantCdnOrigin -CdnType $cdnType -OriginUrl $_ -Confirm:$false }}
}

Paul.

OAuth On-Behalf-Of Flow: Getting a user access token without user interaction. Includes ADAL sample.

OAuth 2.0 (and hence Azure Active Directory) provides the On-Behalf-Of flow to support obtaining a user access token for a resource with only a user access token for a different resource – and without user interaction.
This supports the scenario where a secured Web API acts as an interface to other resources (a.k.a endpoints) secured by the same identity provider and that require user context. As a practical example, a mobile client accesses some resources via a middle tier API which provides services such as data processing, caching, API simplification/optimisation, joining of datasets, etc.

The OAuth flow that achieves this is called the On-Behalf-Of flow; this makes sense as we’re facilitating the middle tier to act on behalf of the client when it accesses the resources farther down.

Using the on-behalf-of flow to access a resource via a middle tier API

Some background

Authentication with an OAuth 2.0 identity provider (such as AAD) produces JWT tokens. These tokens include information such as which claims (permissions) the user should be granted and the particular resource at which the token is valid (such as graph.microsoft.com). The OAuth 2.0 framework is specified such that a given token can only ever be valid for a single resource. This means that the token received by an endpoint (such as an Azure App Service Web API) cannot be used to directly authenticate to ‘another resource’. This is because the token’s resource will be that of the Web API and not the ‘other resource’. To see this I recommend checking out jwt.io and cracking open some tokens.

For completeness, the ‘other resource’ could be accessed using app-only authentication if it supports it, and if user context is not required (i.e. the return value will be the same regardless of the user) although this may greatly increase complexity in a multi-tenant scenario.

Configuring AAD for on-behalf-of

Before we get to the code the first hurdle is configuring AAD app registrations correctly. Initially it may be tempting to consider having both the Client and Web API layers utilise a single AAD app registration. After all, they are same holistic ‘app’ and how else can we get a user to consent to the permissions required by the Web API app when there is no interactive interface at that point? The latter point is resolved by explicitly binding the app registrations so that both are consented to as one. I mention how this is done below. By having two app registrations the flexibility of configuration is improved; we can have a Native app registration for the client and a Web API app registration for the Web API, we can have implicit flow configured for one app and not the other, and generally have granular control over configuration. Most vitally, an app registration can’t issue tokens valid for its own resource so two app registrations is a requirement.

I’ll avoid stepping though the configuration of the app registrations here as this is available elsewhere including this GitHub project. I will give a high level overview of what needs to happen.

    1. Create app registration for the Web API
      • Assign permissions to the downstream resources (e.g. Microsoft Graph, a custom Web API, etc)
      • If supporting multi-tenant authentication ensure availableToOtherTenants is set to true in the manifest
    2. Create app registration for the Client
      • Assign permissions to the app registration created above for the Web API
      • If supporting multi-tenant authentication ensure availableToOtherTenants is set to true in the manifest
    3. Associate app registrations
      • In the manifest for Web API app registration, configure knownClientApplications to reference the App ID for the app registration created for the Client. E.g. "knownClientApplications": ["9XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXc"]
        This binds the app registrations such that the Web API app registration is consented to as part of a single consent dialog displayed to a user when they authenticate to the Client app registration.
Before and after the app registrations are associated. Note how ‘Access Mobile App Backend’ is no longer present and instead is expanded to show the individual permissions required by that app.

Access Token Broker code

The following code is a .NET example of how to use the Active Directory Authentication Library (ADAL) to achieve the On-Behalf-Of flow.

Credits and further reading

This GitHub project was a very useful resource and a recommended starting point:
https://github.com/Azure-Samples/active-directory-dotnet-webapi-onbehalfof-ca

Microsoft docs OAuth 2.0 On-Behalf-Of flow
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols-oauth-on-behalf-of

OAuth 2.0 specification
https://tools.ietf.org/html/rfc6749

Redis Token Cache example
https://blogs.msdn.microsoft.com/mrochon/2016/09/19/using-redis-as-adal-token-cache/

Vardhaman Deshpande – always helpful
http://www.vrdmn.com/

I was inspired to write this post not because this information isn’t available but because the information is hard to find if you aren’t familiar with the term “On-Behalf-Of”. Hopefully this post will be found by those of you searching for terms like “trade access token for new resource”, “change token resource”, “use access token with multiple resources/endpoints”, “access Microsoft Graph via Web API”, etc.

Paul.

User photos in Office 365

The user photo story in Office 365 is not so straight forward. Photos are stored in Active Directory (AD) on-premises, Azure Active Directory (AAD), Exchange Online (EXO), SharePoint Online (SPO), and at first appearances possibly elsewhere as well (where does my Delve profile picture live, what about my Skype for Business (SfB) avatar?).

I have put together a flow diagram to represent how this actually works. It aims to demonstrate where user photos are stored and where different applications fetch user photos from (if they don’t store the images), and leads to some recommendations about user photo synchronisation.

Please note the date of this article (August 2016) and be conscious that Office 365 is changing rapidly and the following recommendations may have changed (e.g. Prior to the Delve user profile page, the SharePoint user profile page referenced images stored in SharePoint rather than Exchange. Changes such as these will continue to evolve).

User photos: the diagram

User photos flow in Office 365
User photos flow in Office 365

Where applications store and fetch user photos

Photo Location

Comments

Size

Is source?

On-premises AD DS in the thumbnailPhoto attribute

100Kb maximum

Recommended to be

96×96 or 48×48

Yes

Azure AD in the thumbnailPhoto attribute

100Kb maximum

Usually synced from AD DS via Azure AD Connect

Recommended to be

96×96 or 48×48

No.

Sync from AD

Exchange Online as property of the mailbox

500Kb

Provided manually by users or a bulk import can be scripted if source photos can be located and named appropriately.

If not provided, Exchange will reference the AAD thumbnailPhoto in some instances but only if the thumbnailPhoto is less than 10Kb.

Does not sync back to AD

Recommended to be

648×648

Yes

SharePoint Online ‘User Photos’ library

Three renditions of the EXO photo are automatically created in SharePoint after upload to EXO.

It generally takes up to 72 hours to see changes to EXO photo here. Sometimes we see that a user must ‘touch’ their profile before the sync will be performed.

NOTE: Updating user profile photo via Delve profile is actually updating EXO profile photo and not performing any actions directly in SharePoint Online.

Small is 48 x 48,

Medium is 72 x 72,

Large changes depending on the source image but is always square. I have seen as small as 120 x 120 and as large as 300 x 300. PnP image upload solution uploads these as 200 x 200.

No.

Sync from EXO

Skype for Business

Does not store any images

Uses the high resolution Exchange image if available, otherwise uses the AD thumbnailPhoto

EXO image or AD thumbnailPhoto

No.

Read from EXO

Delve user profile

Does not store any images

Uses the high resolution Exchange image if available, otherwise uses the AD thumbnailPhoto

EXO image or AD thumbnailPhoto

No.

Read from EXO

Yammer

Also stores its own photo. Out of scope of this discussion for now.

Yes

Likely issues and resolutions

Issue

Resolution

Exchange Online user photo is low quality (and in turn so is the SPO photo and SfB photo)

The source image coming from AD was/is low quality.

EXO user photos can be updated by users individually or if high res source photos are available this import can be scripted.

Source images should be jpg of 648×648 (resizing and compression can also be scripted)

Exchange Online user photo is high quality but SfB photo is low quality

High resolution photos from Exchange will be used as long as both Exchange and Sfb/Lync are of new enough versions (2013 or greater) and SfB is configured to allow all photos (not just those from AD).

NB. If a user doesn’t have a mailbox (e.g. not licenced) then they will be displayed using the AD photo

There is no Exchange Online user photo (and in turn there is no SPO photo or SfB photo)

A photo has not been imported to the user’s EXO mailbox and the AAD thumbnailPhoto either doesn’t not contain an image or that image is greater than 10Kb.

Import of photos up to 500Kb to EXO mailbox can be scripted (the source images could be on a file share, or AAD).

Changes to user photos are reflected quickly in Exchange and Skype but take days to replicate to SPO

Exchange to SPO synchronisation is a periodic process and can take up to 72 hours.

A custom solution can perform this replication on demand (e.g. at the same time EXO user photos are set)

User photos changed in other systems which update AD are not reflected in EXO, SPO, SfB.

E.g. A user in an on-premises SharePoint farm updates their user photo

When AD is updated, it is synchronised with AAD but that is as far as it gets as the “sync” from AAD to EXO is one-off import rather than a Sync.

Unlikely to be desirable to create a custom sync relationship here as users will want to be able to update EXO directly and won’t want their photo’s overwritten

User photos updated in EXO aren’t replicated to other systems which share an AD.

E.g. An on-premises SharePoint farm

The user photo in EXO is not synched back to AD – it can’t be consistently as the AD thumbnailPhoto attribute only supports photos up to 100Kb where EXO supports larger images.

Potential for a custom solution to sync images back to AD after having resized/compressed them to <100kb – However general recommendation is that AD thumbnailPhoto optimal size is 10Kb and 96×96.

Recommendations

Use Exchange user photos as the master. Allow users to update their user photos but pre-populate their user photo if possible and before end users are provided any access to the system.

If high resolution photos are available, script import of high resolution photos (648×648) to Exchange Online (see Set-UserPhoto and this and sample script below). These will then be visible in Exchange, in Skype, and, once processed, in SharePoint Online. In a dispersed environment this may have to be managed by many teams rather than trying to compile a single list of all user photos.

Users may then update their user profile photo directly via Outlook or indirectly via their Delve profile.

If synchronisation back to AD is required in order serve other applications (e.g. an on-premises SharePoint farm) then a custom solution could provide synchronisation from EXO to AD but this process should compress and shrink images as the recommended size of thumbnailPhoto images is only 96×96 and 10Kb.

Sample usage of the Set-UserPhoto cmdlet

Paul.


Azure AD app wildcard Reply URL

Azure AD apps (a.k.a Azure Active Directory apps, a.k.a AAD apps) are an essential component when interacting with Office 365 data outside of SharePoint – Mail, Calendar, Groups, etc.

As an O365 developer I have found myself writing JavaScript code against AAD apps (using ADAl.js) and often, especially during development, found myself entering a long list of Reply URLs. Reply URLs must be specified for any location from which authentication to AAD occurs. From a practical standpoint this results in someone (an Azure Administrator) having to update the list of Reply URLs every time a web part is inserted into a page or a new site is provisioned which relies on an Azure AD app.

If this is not done, the user is redirected to Azure login failure with ‘The reply address … does not match the reply addresses configured for the application’.

Error when Reply URL is not correctly specified
Error when Reply URL is not correctly specified

Perhaps the following is documented elsewhere but I have not come across it – a Reply URL can be specified using wildcards!

Using a wildcard Reply URL when configuring an AAD app
Using wildcard Reply URLs when configuring an AAD app

Probably the most common use for this is to end a Reply URL with an asterisk (wildcard) which will permit any URL which begins with the characters preceding it.

e.g. https://tenant.sharepoint.com/*
This example would support any URL coming from any page in SharePoint Online from within the named tenant.

It is also possible to use the wildcard character elsewhere in the Reply URL string.
e.g. https://*.sharepoint.com/*
This example would support any URL coming from any page in SharePoint Online from within *any* tenant.

Armed with this knowledge, be responsible and limit strictly how it is utilised. The implementation of Reply URL is a security feature and it is important that only trusted locations are allowed to interact with your app. I recommend only using wildcard Reply URLs in development environments.

Paul.

How to get your Office 365 Theme appearing immediately

With the addition of Office 365 themes we can provide some company branding throughout the Office 365 suite. But why does it take so long to propagate down throughout the SharePoint sites in the tenant?

Set an Office 365 theme
Set an Office 365 theme

These themes affect the suite bar across the top of the page. I see it often that after setting the Office 365 theme for a tenant, the theme gets applied to Mail, Calendar, Delve, etc but can seem to take a very long time before it appears on SharePoint sites.

Well it is actually just taking a long time to appear on SharePoint sites if you have visited SharePoint recently. The Office 365 theme colours are cached locally in the browser using local storage.

The local storage key that is used is: SPSuiteLinksJson. This appears to store everything that is required to render the entire suite bar.

Interestingly there is another local storage key which appears to store the date at which the suite bar data was cached: SPSuiteLinksDate. Presumably this is used to calculate when the cache should be refreshed from the server. I am not sure exactly how long this is yet. Please comment if you have figured out the cache timeout.

Regardless of its duration, if you want to force end users to get the latest theme more often you have the options of clearing either of these values or modifying the date field appropriately. But really, I can’t imagine why this would be so important although its nice to know that a bit of JavaScript can sort this out if need be.

Paul.

FYI – The value that is stored will be something like the following (taken from a test tenant):

{"SPSuiteVersion":2,"SPIsMobile":false,"CssUrl":"https://prod.msocdn.com/16.00.0915.008/en-US/css/shellg2coremincss_40fd4be6.css","JsUrl":"https://prod.msocdn.com/16.00.0915.008/en-US/JSC/CoreMinShellG2Bundle.js","NavBarData":{"AboutMeLink":{"BrandBarText":null,"Id":"ShellAboutMe","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":null,"Text":"About me","Title":"Go to the My profile page","Url":"https://tenant-my.sharepoint.com/person.aspx"},"AdminLink":{"BrandBarText":null,"Id":"ShellAdmin","MenuName":"Admin centers","ServiceId":null,"SubLinks":null,"TargetWindow":null,"Text":"Admin","Title":"Go to the Office 365 admin center","Url":"https://portal.office.com/admin/default.aspx"},"AppsImage":null,"AppsLinks":null,"ClientData":"{\"AddBusinessUserUrl\":null,\"AdminHelpUrlFormat\":\"http:\\/\\/o15.officeredir.microsoft.com\\/r\\/rlidOfficeWebHelp?p1=O365ENTADMIN&clid=1033&ver=15&services=INTUNE_O365%2cYAMMER_ENTERPRISE%2cMCOSTANDARD%2cSHAREPOINTSTANDARD%2cEXCHANGE_S_STANDARD&p2=O365&HelpID={HelpID}\",\"AppHeaderLinkText\":null,\"AppHeaderLinkUrl\":null,\"AppLauncherErrorHelpUrl\":\"http:\\/\\/o15.officeredir.microsoft.com\\/r\\/rlidOfficeWebHelp?p1=O365ENTADMIN&clid=1033&ver=15&services=INTUNE_O365%2cYAMMER_ENTERPRISE%2cMCOSTANDARD%2cSHAREPOINTSTANDARD%2cEXCHANGE_S_STANDARD&p2=O365&HelpID=O365E_AppLTrustedSites\",\"AppSearchEnabled\":false,\"AppsCustomizationDisabled\":false,\"AppsDiscoverabilityDisabled\":false,\"AppsDragAndDropDisabled\":false,\"AppsGetAllAppTilesEnabled\":false,\"AppsPrePinnedDisabled\":false,\"AppsResizingDisabled\":true,\"AppsUpdateTimeSpan\":3600000,\"CDNUrl\":\"https:\\/\\/prod.msocdn.com\\/16.00.0915.008\\/en-US\",\"CardBundleJS\":\"https:\\/\\/prod.msocdn.com\\/16.00.0915.008\\/en-US\\/JSC\\/CardBundle.js\",\"ChatNotificationsDisabled\":false,\"ClearUserThemeCacheUrls\":\"[{\\\"WorkloadId\\\":\\\"AdminPortal\\\",\\\"WorkloadUrl\\\":\\\"https:\\/\\/portal.office.com\\/misc\\/expirecookies.aspx\\\",\\\"RequestType\\\":\\\"img\\\",\\\"NoRequestInSameWorkload\\\":true},{\\\"WorkloadId\\\":\\\"Sharepoint\\\",\\\"WorkloadUrl\\\":\\\"https:\\/\\/tenant-my.sharepoint.com\\/_layouts\\/15\\/ClearLocalCache.aspx\\\",\\\"RequestType\\\":\\\"iframe\\\",\\\"NoRequestInSameWorkload\\\":false},{\\\"WorkloadId\\\":\\\"Sharepoint\\\",\\\"WorkloadUrl\\\":\\\"https:\\/\\/tenant.sharepoint.com\\/_layouts\\/15\\/ClearLocalCache.aspx\\\",\\\"RequestType\\\":\\\"iframe\\\",\\\"NoRequestInSameWorkload\\\":false}]\",\"ClientTelemetryEnabled\":false,\"ClientTelemetrySource\":\"O365SuiteUX\",\"ClientTelemetryToken\":\"\",\"ClientTelemetryUIVersion\":\"1000\\/1.6.5.0\",\"ClientTelemetryUrl\":\"\",\"CollectorIds\":[\"SharePoint\",\"Graph\",\"FirstParty\",\"LineOfBusiness\"],\"ConsumerWorkloadUrls\":null,\"DefaultSavedUserUrl\":null,\"DisplayNameFormat\":null,\"EditPhotoPopupUrl\":\"https:\\/\\/outlook.office365.com\\/ecp\\/PersonalSettings\\/EditAccount.aspx?chgPhoto=1&exsvurl=1&realm={0}\",\"ExchangeWLADUpdateInterval\":28800000,\"FlexPaneDisabled\":false,\"GallatinLegalAlertEnabled\":false,\"GroupsDisabled\":true,\"HasEXOLicense\":true,\"HasSkypeLicense\":true,\"IdentitySwitcherEnabled\":false,\"ImmersiveProfileUrl\":\"profile\\/\",\"ImmersiveSettingsUrl\":\"settings\\/\",\"IsConsumerShell\":false,\"IsGuestMode\":false,\"IsIWDelveLinkPresent\":true,\"IsNFDDisabled\":false,\"IsNFDOnO365SuiteServiceEnabled\":false,\"IsO365SuiteServiceEnabled\":false,\"IsPartnerResellerPage\":false,\"IsRTL\":false,\"IsTenantDirSyncEnabled\":false,\"LoadUserThemesUrl\":\"https:\\/\\/portal.office.com\\/data.theme?action=tu&l=en-US&tt=G2&cdnver=16.00.0915.008\",\"LocalNotificationsDisabled\":false,\"LogArgLength\":1024,\"LogLevelSwitches\":[false,true,true,true],\"LogLevelSwitchesForPage\":null,\"LogUrl\":\"https:\\/\\/clientlog.portal.office.com\\/l.l\\/\",\"LyncIntegrationDisabled\":true,\"LyncIntegrationUrl\":\"https:\\/\\/swx.cdn.skype.com\\/shared\\/v\\/1.2.3\\/SkypeBootstrap.min.js\",\"MePhotoCachingDisabled\":false,\"MobileShellDisabled\":true,\"MultipleAADSwitchingEnabled\":false,\"MyAccountEnabled\":false,\"MyAccountUrl\":\"account\\/\",\"MyAppsUrl\":\"https:\\/\\/portal.office.com\\/myapps\",\"NeutralGrayColors\":[\"000000\",\"212121\",\"333333\",\"666666\",\"767676\",\"A6A6A6\",\"C8C8C8\",\"EAEAEA\",\"F4F4F4\",\"F8F8F8\",\"FFFFFF\"],\"NeutralStatusColors\":[\"A80F22\",\"D83B01\",\"2A8DD4\",\"107C10\"],\"NewAppLauncherHelpUrl\":null,\"NewAppNotificationEnabled\":false,\"NewMailNotificationsDisabled\":false,\"NoResultsHelpLinkUrl\":\"https:\\/\\/support.office.com\",\"NotificationsFlexPaneEnabled\":true,\"NotificationsSettingsV2Enabled\":false,\"PUID\":\"1003BFFD91368966\",\"PortalUrl\":\"https:\\/\\/portal.office.com\\/\",\"ProfileAboutCardApiUrl\":\"api\\/profile\\/data\",\"ProfileDoughboyUrl\":\"\\/images\\/profile\\/doughboy.png\",\"ProfileHeaderBGUrl\":\"\\/images\\/profile\\/profile-header-bg.jpg\",\"ProfileOrgChartCardApiUrl\":\"api\\/profile\\/org\",\"ProfilePaneEnabled\":false,\"RemindersNotificationDisabled\":false,\"RenderAsyncDisabled\":false,\"ResponsiveShellEnabled\":false,\"SHSID\":\"0822f3b5-7e00-4401-8436-34dc280519a9\",\"SavedUserUrl\":null,\"SettingsCardApiUrl\":\"api\\/settings\\/cards\",\"SettingsFlexPaneEnabledWorkloads\":\"ShellAdmin;AdminPortal\",\"SettingsPaneEnabled\":false,\"SettingsSearchClassifierUrl\":\"https:\\/\\/prod.msocdn.com\\/16.00.0915.008\\/JS\\/SettingSearch\\/SettingsSearchClassifier_en.js\",\"SettingsSearchNlrtUrl\":\"https:\\/\\/prod.msocdn.com\\/16.00.0915.008\\/en-US\\/JS\\/SettingSearch\\/MicrosoftNaturalLanguageRuntime.js\",\"SettingsSessionStorageEnabled\":false,\"ShellCoreCSS\":\"https:\\/\\/prod.msocdn.com\\/16.00.0915.008\\/en-US\\/css\\/shellg2corecss_9037d638.css\",\"ShellCoreCSSResourceKey\":\"shellg2corecss\",\"ShellCoreJS\":\"https:\\/\\/prod.msocdn.com\\/16.00.0915.008\\/en-US\\/JSC\\/CorePrimeShellG2Bundle.js\",\"ShellCoreJSResourceKey\":\"shellcoreprimeg2m\",\"ShellPlusCSS\":\"https:\\/\\/prod.msocdn.com\\/16.00.0915.008\\/en-US\\/css\\/shellg2pluscss_1db8cf27.css\",\"ShellPlusCSSResourceKey\":\"shellg2pluscss\",\"ShellPlusJS\":\"https:\\/\\/prod.msocdn.com\\/16.00.0915.008\\/en-US\\/JSC\\/O365ShellG2Plus.js\",\"ShellPlusJSResourceKey\":\"shellplusg2m\",\"ShellPlusNarrowCSS\":\"https:\\/\\/prod.msocdn.com\\/16.00.0915.008\\/en-US\\/css\\/shellg2plusncss_c867ccef.css\",\"ShellPlusNarrowCSSResourceKey\":\"shellg2plusncss\",\"ShellPlusNarrowJS\":\"https:\\/\\/prod.msocdn.com\\/16.00.0915.008\\/en-US\\/JSC\\/O365ShellG2PlusN.js\",\"ShellPlusNarrowJSResourceKey\":\"shellplusg2n\",\"ShellPlusTouchCSS\":\"https:\\/\\/prod.msocdn.com\\/16.00.0915.008\\/en-US\\/css\\/shellg2plustcss_846fbfc5.css\",\"ShellPlusTouchCSSResourceKey\":\"shellg2plustcss\",\"ShellPlusTouchJS\":\"https:\\/\\/prod.msocdn.com\\/16.00.0915.008\\/en-US\\/JSC\\/O365ShellG2PlusT.js\",\"ShellPlusTouchJSResourceKey\":\"shellplusg2t\",\"ShellPlusWideCSS\":\"https:\\/\\/prod.msocdn.com\\/16.00.0915.008\\/en-US\\/css\\/shellg2pluswcss_5c4da058.css\",\"ShellPlusWideCSSResourceKey\":\"shellg2pluswcss\",\"ShellPlusWideJS\":\"https:\\/\\/prod.msocdn.com\\/16.00.0915.008\\/en-US\\/JSC\\/O365ShellG2PlusW.js\",\"ShellPlusWideJSResourceKey\":\"shellplusg2w\",\"ShowMailNotificationCount\":false,\"ShowNewAppLauncher\":false,\"ShowNewMyAppsLink\":true,\"SkypeApiKey\":\"b32c0389-1143-4a42-b15f-7b860f9eafbb\",\"SocialNotificationsDisabled\":true,\"SuiteServiceCallsDisabled\":false,\"SuiteServiceUrl\":\"https:\\/\\/outlook.office365.com\\/owa\\/service.svc\",\"SwitchToBusinessUserUrl\":null,\"SystemNotificationsDisabled\":false,\"TID\":\"2fb81f0d-c6ef-4c83-b35c-9553261f9d9b\",\"TenantPrimaryColorShades\":[\"149B14\",\"18BD19\",\"18BD19\",\"1DE01D\",\"72D071\",\"C7E2C7\",\"FDFEFD\",\"FFFFFF\",\"FFFFFF\"],\"TenantThemeColors\":[\"1DE01D\",\"0CA597\",\"FFFFFF\",\"FFFFFF\"],\"TenantThemeCssUrl\":\"https:\\/\\/portal.office.com\\/data.theme?action=tc&tc=1DE01D|0CA597|FFFFFF|FFFFFF&tt=G2&tv=40357e1c-4ba4-4ff1-94ae-6df34f02fe07&l=en-US\",\"ThemeCssUrl\":\"https:\\/\\/portal.office.com\\/data.theme?action=tc&tc=1DE01D|0CA597|FFFFFF|FFFFFF&tt=G2&tv=40357e1c-4ba4-4ff1-94ae-6df34f02fe07&l=en-US\",\"ThemePanelEnabled\":true,\"UID\":\"f0f20750-f14a-416b-84da-03119aa4ac1e\",\"UPN\":\"ccdev5@tenant.onmicrosoft.com\",\"UserPersonalizationAllowed\":false,\"UserThemeId\":\"Base\",\"UserThemesPanelUrl\":null,\"UserThemesSettingsPageUrl\":\"https:\\/\\/portal.office.com\\/EditProfile15.aspx?serviceId=ThemeItem\",\"WorkloadId\":\"Sharepoint\"}","CommunityLink":{"BrandBarText":null,"Id":"ShellCommunity","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":"_blank","Text":"Community","Title":"Community","Url":"http://g.microsoftonline.com/0BX20en/142"},"CompanyDisplayName":"Content and Code","CorrelationID":"0822f3b5-7e00-4401-8436-34dc280519a9","CultureName":"en-US","CurrentMainLinkElementID":"ShellSharepoint","CurrentWorkloadHelpSubLinks":null,"CurrentWorkloadSettingsLink":null,"CurrentWorkloadSettingsSubLinks":null,"CurrentWorkloadUserSubLinks":null,"Dimensions":{"Top":50},"DownArrowImage":null,"DownWhiteArrowImage":null,"FeedbackLink":{"BrandBarText":null,"Id":"ShellFeedback","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":"_blank","Text":"Feedback","Title":null,"Url":"https://portal.office.com/SendSmile?wid=2"},"FlightName":"15GA,Exp180917IngestionFailedStatusTreat1,Exp130705C,Exp170859ihdc100,SE80186HRCSetupRedirect,SE130231CFRV2,SE130238GeminiSignupUI,SE160251CFRV2Detail,SE160252CFRV2Prefetch,SE160258ShellDelveLink,SE190296ShellVideoLink,SE190331GeminiPurchaseUI,SE190354IWSignupAlertsOn,SE190448AADUXChangePassword,SE190549NotificationsFlexPane,SE190532AggregatedServiceHealth,SE200626ShellSwayLink,SE200631PartnerFeedback,SE200681SwitchReadFromAPIService,SE220756PartnerClientTroubleshoot,SE220826AppStore,SE220829LOBApps,SE220825Office2016PreviewForPc,SE220822IntuneMDMStaged,SE240791PartnerUsageTracking,SE240809ShellServiceThemeCache,SE240810O365ImmersiveSettings,SE240827CortanaAccessSettings,SE240829ShellClassNoteBookLink,SE240843PartnerSettingsPageFeature,SE240844HelpFlexPane,SE240866MobileAdmin,SE240880OAuthAuthenticationForAPIService,SE240881GeminiCatalogUI,SE240883ShellNewMyAppsLink,SE250903CFRV3,SE250928ShellPowerBILink,SE260930UnifiedSetup,SE240865ShellStaffNoteBookLink,SE271003CFRV3CsvExportEnabled,SE271005CutoverProxy,SE271017SwitchReadFromAPIService,SE271018CFRV3TS,SE271014AdvSetupImap,SE281032AetherSubscriptionsUI,SE291071PartnerResellerMultiPartner,SE281034OfficeHome3,Exp9015C","FlipHelpIcon":false,"FooterCopyrightLogoTitle":null,"FooterCopyrightText":null,"FooterICPLink":null,"FooterLogoImage":null,"HasTenantBranding":true,"HelpImage":null,"HelpLink":{"BrandBarText":null,"Id":"HelpLink","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":"_blank","Text":"Help","Title":null,"Url":"&services=INTUNE_O365%2cYAMMER_ENTERPRISE%2cMCOSTANDARD%2cSHAREPOINTSTANDARD%2cEXCHANGE_S_STANDARD&p2=O365"},"ImageClusterUrl":null,"IsAuthenticated":true,"LegalLink":{"BrandBarText":null,"Id":"ShellLegal","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":"_blank","Text":"Legal","Title":"Legal","Url":"http://g.microsoftonline.com/0BX20en/721"},"LogoIconID":"o365logo","LogoImage":null,"LogoNavigationUrl":"https://portal.office.com/Home","LogoThemeableImage":null,"MenuTitleText":null,"MyProfileUrl":"https://portal.office.com/EditProfile15.aspx?ServiceID=LanguageItem","NavBarAriaLabel":null,"NotificationsBellIconImage":null,"NotificationsCountLabelText":null,"NotificationsHighIconImage":null,"NotificationsLowIconImage":null,"NotificationsMediumIconImage":null,"NotificationsPopupHeaderText":null,"NotificationsProgressIconImage":null,"O365SettingsLink":{"BrandBarText":null,"Id":"ShellO365Settings","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":null,"Text":"Office 365 settings","Title":null,"Url":"https://portal.office.com/settings/"},"PartnerLink":null,"PoweredByText":"powered by {0}","PrivacyLink":{"BrandBarText":null,"Id":"ShellPrivacy","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":"_blank","Text":"Privacy","Title":"Privacy","Url":"http://g.microsoftonline.com/0BX20en/138"},"SPO_MySiteHostUrl":null,"SPO_RootSiteUrl":null,"SessionID":"e88c267e-5b6e-49ab-8f2d-766510e25c46","SettingsImage":null,"SignOutLink":{"BrandBarText":null,"Id":"ShellSignout","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":null,"Text":"Sign out","Title":"Sign out of Office 365 and return to the Sign-in page","Url":"https://login.microsoftonline.com/logout.srf?ct=1443035468&rver=6.4.6456.0&lc=1033&id=271346"},"TenantBackgroundImageUrl":null,"TenantLogoNavigationUrl":null,"TenantLogoUrl":{"BrandBarText":null,"Id":null,"MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":null,"Text":null,"Title":"Content and Code","Url":"data:image/png;base64,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"},"ThemeColors":null,"TransparentImageUrl":null,"TruncatedUserDisplayName":null,"UpArrowImage":null,"UseSPOBehaviors":false,"UserDisplayName":"Content and Code Dev 5","WorkloadLinks":[{"BrandBarText":"Outlook","Id":"ShellMail","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":null,"Text":"Mail","Title":"Go to your email","Url":"https://outlook.office365.com/owa/?realm=tenant.onmicrosoft.com&exsvurl=1&ll-cc=1033&modurl=0"},{"BrandBarText":"Outlook","Id":"ShellCalendar","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":null,"Text":"Calendar","Title":"Go to your calendar","Url":"https://outlook.office365.com/owa/?realm=tenant.onmicrosoft.com&exsvurl=1&ll-cc=1033&modurl=1"},{"BrandBarText":"Outlook","Id":"ShellPeople","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":null,"Text":"People","Title":"Go to People and contacts","Url":"https://outlook.office365.com/owa/?realm=tenant.onmicrosoft.com&exsvurl=1&ll-cc=1033&modurl=2"},{"BrandBarText":null,"Id":"ShellNewsfeed","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":null,"Text":"Newsfeed","Title":"Go to your Newsfeed","Url":"https://tenant-my.sharepoint.com/default.aspx"},{"BrandBarText":null,"Id":"ShellDocuments","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":null,"Text":"OneDrive","Title":"Go to OneDrive for Business","Url":"https://tenant-my.sharepoint.com/personal/ccdev5_tenant_onmicrosoft_com/_layouts/15/start.aspx#/_layouts/15/onedrive.aspx"},{"BrandBarText":null,"Id":"ShellSites","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":null,"Text":"Sites","Title":"Go to team sites","Url":"https://tenant-my.sharepoint.com/personal/ccdev5_tenant_onmicrosoft_com/_layouts/15/start.aspx#/Social/Sites.aspx"},{"BrandBarText":"Outlook","Id":"ShellTasks","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":null,"Text":"Tasks","Title":"Go to Outlook Web App for Tasks","Url":"https://outlook.office365.com/owa/?realm=tenant.onmicrosoft.com&exsvurl=1&ll-cc=1033&modurl=3"},{"BrandBarText":null,"Id":"ShellOfficeGraph","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":null,"Text":"Delve","Title":"Go to Delve","Url":"https://tenant-my.sharepoint.com/_layouts/15/me.aspx?origin=shell"},{"BrandBarText":null,"Id":"ShellVideo","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":null,"Text":"Video","Title":"Go to Video for Office 365 to share videos","Url":"https://tenant.sharepoint.com/portals/hub/_layouts/15/videohome.aspx"},{"BrandBarText":null,"Id":"ShellWordOnline","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":"_blank","Text":"Word Online","Title":"Go to Word Online","Url":"https://office.live.com/start/Word.aspx?auth=2"},{"BrandBarText":null,"Id":"ShellExcelOnline","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":"_blank","Text":"Excel Online","Title":"Go to Excel Online","Url":"https://office.live.com/start/Excel.aspx?auth=2"},{"BrandBarText":null,"Id":"ShellPowerPointOnline","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":"_blank","Text":"PowerPoint Online","Title":"Go to PowerPoint Online","Url":"https://office.live.com/start/PowerPoint.aspx?auth=2"},{"BrandBarText":null,"Id":"ShellOneNoteOnline","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":"_blank","Text":"OneNote Online","Title":"Go to OneNote Online","Url":"https://www.onenote.com/notebooks?auth=2"},{"BrandBarText":null,"Id":"ShellSway","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":"_blank","Text":"Sway","Title":"Go to Sway","Url":"https://www.sway.com?auth_pvr=OrgId&auth_upn=ccdev5@tenant.onmicrosoft.com"},{"BrandBarText":null,"Id":"ShellOfficeStore","MenuName":null,"ServiceId":null,"SubLinks":null,"TargetWindow":"_blank","Text":"Office 365 Store","Title":null,"Url":"https://portal.office.com/store"}]},"SharedCSSTouchWideUrl":"https://prod.msocdn.com/16.00.0915.008/en-US/css/shellg2corewcss_8b4fb7b2.css","SharedJSTouchWideUrl":"https://prod.msocdn.com/16.00.0915.008/en-US/JSC/CoreShellG2BundleW.js","SharedCSSTouchNarrowUrl":"https://prod.msocdn.com/16.00.0915.008/en-US/css/shellg2corencss_18d28bae.css","SharedJSTouchNarrowUrl":"https://prod.msocdn.com/16.00.0915.008/en-US/JSC/CoreShellG2BundleN.js","SharedCSSTouchDeviceUrl":"https://prod.msocdn.com/16.00.0915.008/en-US/css/shellg2coretcss_ba6efae3.css","SharedJSTouchDeviceUrl":"https://prod.msocdn.com/16.00.0915.008/en-US/JSC/CoreShellG2BundleT.js","TenantPrimaryColorShades":["149B14","18BD19","18BD19","1DE01D","72D071","C7E2C7","FDFEFD","FFFFFF","FFFFFF"],"UserThemePrimaryColorShades":["104A7D","0D62AA","106EBE","0078D7","2B88D8","71AFE5","C7E0F4","DEECF9","EFF6FC"],"UserPersonalizationAllowed":false,"ThemeVersion":"G2","ShellRequestId":"aadc309d-b069-3000-9493-98edca352038"}

Paul.

Calling the Office 365 Unified API from JavaScript using ADAL.js

The goal of this post is to provide very basic ‘hello world’ example of how to call the Office 365 Unified API (aka Graph API) using JavaScript and the ADAL.js library. This has recently become possible (May 2015) now that CORS is supported by the Office 365 APIs (most of the individual endpoints support it as well as the unified API).

The ADAL library simplifies the process of obtaining and caching the authentication tokens required to retrieve data from Office 365. It is possible to avoid the ADAL library and handle this yourself, although I would recommend doing so as a learning exercise only.

I failed to find a simple example of how to achieve this, my search results often filled with examples of calling the APIs from server-side code or else utilising the Angular.js framework. This example is based on a more complex example.

The following snippet will log to the browser console the results of a call the to files endpoint of the Office 365 unified API, which will return a JSON object containing information about the files in the current users’ OD4B.

Before it will work you must complete the following steps (as described in detail here):

  1. Register an Azure Active Directory App. Note that *every* Office 365 subscriptions comes with AAD and supports the creation of an app
  2. Associate the required ‘permissions to other services’, in this case ‘Read users files’ via the Office 365 Unified API
  3. Allow implicit flow

Not covered explicitly in the above article but also critical are the following steps:

  • Get the App’s Client ID and copy it into the snippet
The App Client ID
The App Client ID
  • Get the Azure Active Directory subscription ID and copy it into the snippet
The subscription ID in Azure Active Directory
The subscription ID in Azure Active Directory

Once the above steps have been completed, you can try out the snippet by embedding in a Script Editor web part, or you can run it externally to SharePoint as part of, say, a provider hosted app.

NOTE: I found that the call to files endpoint is failing for certain users. I am still unsure whether this is due to external vs internal users (is working for internal [.onmicrosoft.com] users) or whether it could be licencing issue. The /beta/me endpoint is working in all cases.

Paul.

GLOSSARY

CORS: Cross-Origin Resource Sharing
ADAL: Active Directly Authentication Library
OD4B: OneDrive 4 Business

Suite Bar logo is not displayed after applying theme

In SharePoint Online I have detected a bug that prevents the suite bar logo (configured in the o365 tenant admin center) from being rendered after applying a theme with a non-default colour scheme. The logo is not just hidden or not visible; the HTML is no longer rendered to the page at all.

NOTE: This issue has been raised with Microsoft and they have acknowledged it as a bug. They have told is that a fix is planned for June 2015 and should take a couple of weeks to reach all tenants.

Update 06/08/2015

I was advised last week that a fix this issue had officially started rolling out. Today I can confirm that one of our test tenants no longer exhibits this behavior! At the time of writing some of our tenants have not yet received the fix, but it is definitely rolling out and should be with you soon if not already.

When exactly is this occurring?

There is a colour scheme file that nominated as the default for each master page. Only when using this colour scheme in conjunction with the given master page will the logo be present. Specifically, only a colour scheme which is nominated as the default colour scheme will show the suite navigation logo.

This is unrelated to any customisation work and can be observed on any OOTB SharePoint Online publishing site.

The default colour scheme for a given master page is nominated within that master page’s preview file. The preview file is the one which sits alongside the master page in the master page gallery and has the same name name as the master page but with an extension of ‘.preview’ instead of ‘.master’ (e.g., seattle.preview). The first line of the contents of this file identifies the default colour scheme for the master page. When this colour scheme is applied, the theming engine assumes that the CSS is already themed appropriately and does *not* act.

Identifying the default colour scheme for a master page
Identifying the default colour scheme for a master page

I still want the logo on every page, what can we do?

  1. As I have said, a fix is on the way – just wait a few months!
  2. We can inject the logo using JavaScript (or CSS, but we were unable to achieve a CSS only solution which meets our responsive design requirements)
    • It is against best practice
    • This approach is brittle as the suite bar is a living, changing thing – Microsoft is updating it all the time and any changes they make will likely break this functionality.
    • The logo will no longer be configurable via tenant admin settings.
  3. We can create and use multiple master pages – one for every theme colour
    • Partially defeats the purpose of master pages which is to maintain all common pages elements in a single place.
    • Master pages may fall out of synch
    • Adds complexity for support

I want to reproduce this myself, how do I do it?

  1. Create an new site collection in a tenant that has a suite navigation logo configured, using the Publishing Site template
  2. Note that the logo is present
  3. Via Site Settings, “Change the look”
  4. Choose the “Office” composed look
    [Specifically, we want the seattle master page and colour pallette 001]
  5. Change the colour scheme to anything except the default
  6. Apply the look
  7. Note that the logo is no longer present
  8. Re-apply the theme with the default colour scheme and logo will reappear

 

Paul.

SharePoint Online remote authentication (and Doc upload)

The SharePoint REST API is touted as being the tool to provide inter-platform integration with SharePoint Online. However, outside of .NET the authentication piece is not so straightforward. App authentication solves this issue for registered apps but I want to show how remote user authentication can be achieved, regardless of platform.

The goal of this post is to provide examples of the HTTP requests which need to be made in order to authenticate SharePoint Online. It then provides an example of using the same technique to upload a document and update metadata just to prove it all works 🙂

The type of applications where this kind of approach may be necessary include: a Java application, a PHP application, or JavaScript application where there is otherwise no SharePoint Online authentication context and the decision has been made (for whatever reason) that user authentication is most appropriate (as opposed to app authentication).

Edit: This approach will not work in a JavaScript environment due to cross-domain restrictions enforced by browsers (unless of course you are on the same domain, in which case you don’t need to worry about any of this anyway). The ADAL.js library is available for the cross-domain JS scenario. I have posted an example here: http://paulryan.com.au/2015/unified-api-adal/

I wrote about using the SharePoint REST API here (and background here, and here).

I will be providing examples of the requests using the ‘Advanced REST Client’ Google Chrome extension.

Authenticate

The authentication piece comes in a few steps:

  • Get the security token
  • Get the access token
  • Get the request digest

Get the security token

First we must provide a username and password of a user with Contribute access to the Roster Data library and the URL at which we want access to the SharePoint Online Security Token Service.

This is done by POSTing the following XML as the request body to:
https://login.microsoftonline.com/extSTS.srf

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
      xmlns:a="http://www.w3.org/2005/08/addressing"
      xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
  <s:Header>
    <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>
    <a:ReplyTo>
      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    </a:ReplyTo>
    <a:To s:mustUnderstand="1">https://login.microsoftonline.com/extSTS.srf</a:To>
    <o:Security s:mustUnderstand="1"
       xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
      <o:UsernameToken>
        <o:Username>[username]</o:Username>
        <o:Password>[password]</o:Password>
      </o:UsernameToken>
    </o:Security>
  </s:Header>
  <s:Body>
    <t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
      <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
        <a:EndpointReference>
          <a:Address>[endpoint]</a:Address>
        </a:EndpointReference>
      </wsp:AppliesTo>
      <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>
      <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
      <t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>
    </t:RequestSecurityToken>
  </s:Body>
</s:Envelope>

Requesting the security token
Requesting the security token

The response from the request includes the security token needed to get the access token. It is the value which has been stricken out in orange in the image below.

Response including the security token
Response including the security token

Get the access token

Once the security token has been retrieved it must be used to fetch the access token. The can be done by POSTing to the following URL with the security token as the request body:
https://yourdomain.sharepoint.com/_forms/default.aspx?wa=wsignin1.0

Request to fetch the access token, passing the security token
Request to fetch the access token, passing the security token

The response from this request includes couple of cookies which must be passed as headers with all future requests. They are marked with the ‘Set-Cookie’ header. We need the ones beginning with rtFa= and FedAuth=. They can be seen the below image of the response headers.

Response includes the access token cookies
Response includes the access token cookies

Get the request digest

The request digest is a .NET security feature that ensures any update requests are coming from a single session. It must also be included with any POST requests.

The request digest is fetched by POSTing to: https://yourdomain.sharepoint.com/_api/contextinfo
The access token cookies must be included as Cookie headers with the request as shown in the image below.

Request to fetch the request digest, passing access tokens
Request to fetch the request digest, passing access tokens

The response from the request will include the request digest in the XML response as in the image below. The entire contents of the FormDigestValue tag will required, including the date time portion and timezone offset (-0000).

Response containing the request digest value
Response containing the request digest value

Upload a document with metadata

Upload the document

Now that we have all the authentication headers we can make update calls into SharePoint Online as the user whose credentials we originally supplied when fetching the security token.

In order to upload a document perform the following POST request:
https://yourdomain.sharepoint.com/subweb/_api/web/lists/getbytitle(‘list name’)
/rootfolder/files/add(url='filename.csv',overwrite=true)

A number of headers must be send with the request including the access token cookies, the request digest (X-RequestDigest) and the accept header as shown in the image below. The body of the request must contain the content of the document being uploaded.

Request to upload a document to SharePoint Online
Request to upload a document to SharePoint Online

The response of this request contains some minimal metadata about the file and can be safely ignored. However, for completeness here it is.

Response JSON from the document upload request
Response JSON from the document upload request

The unique ID property could be used to fetch the document in order to perform metadata updates rather than URL as done in the following example.

Update document metadata

The final step which needs to take place is update the document in SharePoint with the relevant metadata.

This can be done with yet another POST request. This time to the following URI:
https://yourdomain.sharepoint.com/subweb/_api/web/lists/getbytitle(‘listTitle')
/rootfolder/files/getbyurl(url='serverRelFileUrl')/listitemallfields/validateupdatelistitem

All the headers sent with the previous request must be sent with this request as well. The request body is a JSON object which defines the metadata fields to be updated. The fieldname and fieldValue properties must be updated as required. Note that the fieldname property must be equal to the field internal name not the field display name. An example of this is in the image below.

Request to set metadata on a document in SharePoint
Request to set metadata on a document in SharePoint

The response from this request provides success notification for each individual field update as shown below.

Response from the document metadata update request
Response from the document metadata update request

So, this should now be enough to write an application in any server-side language which supports web requests and work against SharePoint Online. I’d love to see some implementations of this, please comment if you’ve done it.

I’d like to acknowledge the following posts as they were invaluable references:

 

SPC14 Sessions Reviewed (by someone else!)

I have been spending a little time deciding which sessions from the SharePoint Conference 2014 (SPC14) I want to watch over the coming weeks. Unfortunately I was unable to attend this year, but I know from previous years that it truly is an amazing conference with so much great content (and it’s in Vegas baby!).

Anyway, during my searching I came across a blog post (by CJG) which reviews (albeit briefly) every single session. Whaaaa..? I can’t confirm how valid his appraisals are as I’m still yet to get stuck in, however I was so impressed that I had to post a reference to the blog. You can find it here.

SharePoint Conference 2014
SharePoint Conference 2014