First published on TECHNET on Feb 07, 2018
This post is a contribution from Vitaly Lyamin, an engineer with the SharePoint Developer Support team
Accessing SharePoint API’s has never been easier (SPOIDCRL cookie, ACS OAuth, AAD OAuth). Azure AD apps are quickly becoming the standard way of accessing O365 API’s in addition to other API’s. Below are some resources on registering apps and using libraries. Also, there’s a test script that walks through the entire authorization grant flow. The end goal with all OAuth-based authorization is to retrieve the access token to be used in the HTTP request Authorization header (Authorization: Bearer <access token>).
Native Client App
Native app registrations are primarily for devices and services where browser interaction is not needed. One of the biggest benefits is the non-interactive (active) authorization using credentials, Federated IDP assertion or similar.
Links
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-authentication-scenarios#native-application-to-web-api
https://azure.microsoft.com/en-us/resources/samples/active-directory-dotnet-native-headless
Web App / API
Web app registrations are just as they sound – apps on the web. These apps typically use the authorization grant and refresh grant flows and are not intended for devices/services. Once authorized (some permissions scopes require admin consent), the access token is retrieved from the OAuth token endpoint using the authorization code.
Authorization URL
https://login.microsoftonline.com/common/oauth2/authorize?resource=<RESOURCE>&client_id=>CLIENTID>&scope=<SCOPE>&redirect_uri=<REDIRECTURI>&response_type=code&prompt=admin_consent
Access Token URL
https://login.microsoftonline.com/common/oauth2/token
Link
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-authentication-scenarios#web-browser-to-web-application
Libraries
ADAL libraries are available in many different flavors and are quick and easy to implement. There primary purpose is to authorize the user/service to a resource (e.g. SharePoint REST API’s, Graph).
Link
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-authentication-libraries
Other Resources
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-integrating-applications
https://msdn.microsoft.com/en-us/office/office365/howto/getting-started-Office-365-APIs
Test Script (Web App)
<#
.Synopsis
Get access token for AAD web app.
.Description
Authorizes AAD app and retrieves access token using OAuth 2.0 and endpoints.
Refreshes the token if within 5 minutes of expiration or, optionally forces refresh.
Sets global variable ($Global:accessTokenResult) that can be used after the script runs.
.Todo
Add ability to handle refresh token input and access token retrieval without re-authorization.
.Example
The following returns the access token result from AAD with admin consent authorization and caches the result.
PS> .\aad_web.ps1 -Clientid "" -Clientsecret "" -Resource "https://TENANT.sharepoint.com" -Redirecturi "https://localhost:44385" -Scope "" -AdminConsent -Cache
.Example
The following returns the access token result from AAD with admin consent authorization or refreshes the token.
PS> .\aad_web.ps1 -Clientid "" -Clientsecret "" -Resource "https://TENANT.sharepoint.com" -Redirecturi "https://localhost:44385" -Scope "" -AdminConsent
.Example
The following returns the access token result from AAD or from cache, forces refresh so the token is good for an hour and outputs to a file
PS> .\aad_web.ps1 -Clientid "" -Clientsecret "" -Resource "https://TENANT.sharepoint.com" -Redirecturi "https://localhost:44385" -Scope "" -Refresh Force | Out-File c:\temp\token.txt
.PARAMETER ClientId
The AAD App client id.
.PARAMETER ClientSecret
The AAD App client secret.
.PARAMETER RedirectUri
The redirect uri configured for that app.
.PARAMETER Resource
The resource the app is attempting to access (i.e. https://TENANT.sharepoint.com)
.PARAMETER Scope
Permission scopes for the app (optional).
.PARAMETER AdminConsent
Will perform admin consent (optional).
.PARAMETER Cache
Cache the access token in the temp directory for subsequent retrieval (optional).
.PARAMETER Refresh
Options (Yes, No, Force). Will automatically enabling caching if "Yes" or "Force" are used.
Yes: Refresh token if within 5 minutes of expiration if cached token found.
No: Do not refresh and re-authorize.
Force: Forfce a refresh if cached token found.
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]$ClientId,
[Parameter(Mandatory=$true)]
[string]$ClientSecret,
[Parameter(Mandatory=$true)]
[string]$RedirectUri,
[Parameter(Mandatory=$true)]
[string]$Resource,
[Parameter(Mandatory=$false)]
[string]$Scope,
[Parameter(Mandatory=$false)]
[switch]$AdminConsent,
[Parameter(Mandatory=$false)]
[switch]$Cache,
[Parameter(Mandatory=$false)]
[ValidateSet("Yes","No","Force")]
[ValidateNotNullOrEmpty()]
[string]$Refresh = "Yes"
)
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Web
$isCache = $Cache.IsPresent
$isRefresh = (($Refresh -eq "Yes") -or ($Refresh -eq "Force"))
$refreshForce = $Refresh -eq "Force"
if ($isRefresh)
{
$isCache = $true
}
# Don't edit variables below (unless there's a bug)
$clientSecretEncoded = [uri]::EscapeDataString($clientSecret)
$redirectUriEncoded = [uri]::EscapeDataString($redirectUri)
$resourceEncoded = [uri]::EscapeDataString($resource)
$accessTokenUrl = "https://login.microsoftonline.com/common/oauth2/token"
$cacheFilePath = [System.IO.Path]::Combine($env:TEMP, "aad_web_cache_$clientId.json")
$accessTokenResult = $null
$adminConsentText =""
if ($adminConsent)
{
$adminConsentText = "&prompt=admin_consent"
}
$authorizationUrl = "https://login.microsoftonline.com/common/oauth2/authorize?resource=$resourceEncoded&client_id=$clientId&scope=$scope&redirect_uri=$redirectUriEncoded&response_type=code$adminConsentText"
function Invoke-OAuth()
{
$Global:authorizationCode = $null
$form = New-Object Windows.Forms.Form
$form.FormBorderStyle = [Windows.Forms.FormBorderStyle]::FixedSingle
$form.Width = 640
$form.Height = 480
$form.MaximizeBox = $false
$form.MinimizeBox = $false
$web = New-Object Windows.Forms.WebBrowser
$form.Controls.Add($web)
$web.Size = $form.ClientSize
$web.DocumentText = "<html><body style='text-align:center;overflow:hidden;background-image:url(https://secure.aadcdn.microsoftonline-p.com/ests/2.1.6856.20/content/images/backgrounds/0.jpg?x=f5a9a9531b8f4bcc86eabb19472d15d5)'><h3 id='title'>Continue with current user or logout?</h3><div><input id='cancel' type='button' value='Continue' /></div><br /><div><input id='logout' type='button' value='Logout' /></div><h5 id='loading' style='display:none'>Working on it...</h5><script type='text/javascript'>var logout = document.getElementById('logout');var cancel = document.getElementById('cancel');function click(element){document.getElementById('title').style.display='none';document.getElementById('loading').style.display='block';logout.style.display='none';cancel.style.display='none';if (this.id === 'logout'){window.location = 'https://login.microsoftonline.com/common/oauth2/logout?post_logout_redirect_uri=' + encodeURIComponent('$authorizationUrl');}else{window.location = '$authorizationUrl';}}logout.onclick = click;cancel.onclick = click;</script></body></html>"
$web.add_DocumentCompleted(
{
$uri = [uri]$redirectUri
$queryString = [System.Web.HttpUtility]::ParseQueryString($_.url.Query)
if($_.url.authority -eq $uri.authority)
{
$authorizationCode = $queryString["code"]
if (![string]::IsNullOrEmpty($authorizationCode))
{
$form.DialogResult = "OK"
$Global:authorizationCode = $authorizationCode
$Global:authorizationCodeTime = [datetime]::Now
}
$form.close()
}
})
$dialogResult = $form.ShowDialog()
if($dialogResult -eq "OK")
{
$authorizationCode = $Global:authorizationCode
$headers = @{"Accept" = "application/json;odata=verbose"}
$body = "client_id=$clientId&client_secret=$clientSecretEncoded&redirect_uri=$redirectUriEncoded&grant_type=authorization_code&code=$authorizationCode"
$accessTokenResult = Invoke-RestMethod -Uri $accessTokenUrl -Method POST -Body $body -Headers $headers
$Global:accessTokenResult = $accessTokenResult
$Global:accessTokenResultTime = [datetime]::Now
$accessTokenResultText = (ConvertTo-Json $accessTokenResult)
if ($isCache -and ![string]::IsNullOrEmpty($accessTokenResultText))
{
[void](Set-Content -Path $cacheFilePath -Value $accessTokenResultText)
}
Write-Output (ConvertTo-Json $accessTokenResultText)
}
$web.Dispose()
$form.Dispose()
}
function Get-CachedAccessTokenResult()
{
if ($isCache -and [System.IO.File]::Exists($cacheFilePath))
{
$accessTokenResultText = Get-Content -Raw $cacheFilePath
if (![string]::IsNullOrEmpty($accessTokenResultText))
{
$accessTokenResult = (ConvertFrom-Json $accessTokenResultText)
if (![string]::IsNullOrEmpty($accessTokenResult.access_token))
{
$Global:accessTokenResult = $accessTokenResult
return $accessTokenResult
}
}
}
return $null
}
function Invoke-Refresh()
{
$refreshToken = $accessTokenResult.refresh_token
$headers = @{"Accept" = "application/json;odata=verbose"}
$body = "client_id=$clientId&client_secret=$clientSecretEncoded&resource=$resourceEncoded&grant_type=refresh_token&refresh_token=$refreshToken"
$accessTokenResult2 = Invoke-RestMethod -Uri $accessTokenUrl -Method POST -Body $body -Headers $headers
$accessTokenResult.scope = $accessTokenResult2.scope
$accessTokenResult.expires_in = $accessTokenResult2.expires_in
$accessTokenResult.ext_expires_in = $accessTokenResult2.ext_expires_in
$accessTokenResult.expires_on = $accessTokenResult2.expires_on
$accessTokenResult.not_before = $accessTokenResult2.not_before
$accessTokenResult.resource = $accessTokenResult2.resource
$accessTokenResult.access_token = $accessTokenResult2.access_token
$accessTokenResult.refresh_token = $accessTokenResult2.refresh_token
$Global:accessTokenResult = $accessTokenResult
$Global:accessTokenResultTime = [datetime]::Now
$accessTokenResultText = (ConvertTo-Json $accessTokenResult)
if (![string]::IsNullOrEmpty($accessTokenResultText))
{
[void](Set-Content -Path $cacheFilePath -Value $accessTokenResultText)
}
Write-Output (ConvertTo-Json $accessTokenResultText)
}
$accessTokenResult = Get-CachedAccessTokenResult
if ($accessTokenResult -eq $null)
{
Invoke-OAuth
}
elseif ($refreshForce -or (([datetime]::Parse("1/1/1970")).AddSeconds([int]$accessTokenResult.expires_on).ToLocalTime() -lt ([datetime]::Now).AddMinutes(5)))
{
if ($isRefresh)
{
Invoke-Refresh
}
else
{
Invoke-OAuth
}
}
else
{
Write-Output (ConvertTo-Json $Global:accessTokenResult)
}
This post is a contribution from Vitaly Lyamin, an engineer with the SharePoint Developer Support team
Accessing SharePoint API’s has never been easier (SPOIDCRL cookie, ACS OAuth, AAD OAuth). Azure AD apps are quickly becoming the standard way of accessing O365 API’s in addition to other API’s. Below are some resources on registering apps and using libraries. Also, there’s a test script that walks through the entire authorization grant flow. The end goal with all OAuth-based authorization is to retrieve the access token to be used in the HTTP request Authorization header (Authorization: Bearer <access token>).
Native Client App
Native app registrations are primarily for devices and services where browser interaction is not needed. One of the biggest benefits is the non-interactive (active) authorization using credentials, Federated IDP assertion or similar.
Links
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-authentication-scenarios#native-application-to-web-api
https://azure.microsoft.com/en-us/resources/samples/active-directory-dotnet-native-headless
Web App / API
Web app registrations are just as they sound – apps on the web. These apps typically use the authorization grant and refresh grant flows and are not intended for devices/services. Once authorized (some permissions scopes require admin consent), the access token is retrieved from the OAuth token endpoint using the authorization code.
Authorization URL
https://login.microsoftonline.com/common/oauth2/authorize?resource=<RESOURCE>&client_id=>CLIENTID>&scope=<SCOPE>&redirect_uri=<REDIRECTURI>&response_type=code&prompt=admin_consent
Access Token URL
https://login.microsoftonline.com/common/oauth2/token
Link
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-authentication-scenarios#web-browser-to-web-application
Libraries
ADAL libraries are available in many different flavors and are quick and easy to implement. There primary purpose is to authorize the user/service to a resource (e.g. SharePoint REST API’s, Graph).
Link
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-authentication-libraries
Other Resources
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-integrating-applications
https://msdn.microsoft.com/en-us/office/office365/howto/getting-started-Office-365-APIs
Test Script (Web App)
<#
.Synopsis
Get access token for AAD web app.
.Description
Authorizes AAD app and retrieves access token using OAuth 2.0 and endpoints.
Refreshes the token if within 5 minutes of expiration or, optionally forces refresh.
Sets global variable ($Global:accessTokenResult) that can be used after the script runs.
.Todo
Add ability to handle refresh token input and access token retrieval without re-authorization.
.Example
The following returns the access token result from AAD with admin consent authorization and caches the result.
PS> .\aad_web.ps1 -Clientid "" -Clientsecret "" -Resource "https://TENANT.sharepoint.com" -Redirecturi "https://localhost:44385" -Scope "" -AdminConsent -Cache
.Example
The following returns the access token result from AAD with admin consent authorization or refreshes the token.
PS> .\aad_web.ps1 -Clientid "" -Clientsecret "" -Resource "https://TENANT.sharepoint.com" -Redirecturi "https://localhost:44385" -Scope "" -AdminConsent
.Example
The following returns the access token result from AAD or from cache, forces refresh so the token is good for an hour and outputs to a file
PS> .\aad_web.ps1 -Clientid "" -Clientsecret "" -Resource "https://TENANT.sharepoint.com" -Redirecturi "https://localhost:44385" -Scope "" -Refresh Force | Out-File c:\temp\token.txt
.PARAMETER ClientId
The AAD App client id.
.PARAMETER ClientSecret
The AAD App client secret.
.PARAMETER RedirectUri
The redirect uri configured for that app.
.PARAMETER Resource
The resource the app is attempting to access (i.e. https://TENANT.sharepoint.com)
.PARAMETER Scope
Permission scopes for the app (optional).
.PARAMETER AdminConsent
Will perform admin consent (optional).
.PARAMETER Cache
Cache the access token in the temp directory for subsequent retrieval (optional).
.PARAMETER Refresh
Options (Yes, No, Force). Will automatically enabling caching if "Yes" or "Force" are used.
Yes: Refresh token if within 5 minutes of expiration if cached token found.
No: Do not refresh and re-authorize.
Force: Forfce a refresh if cached token found.
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]$ClientId,
[Parameter(Mandatory=$true)]
[string]$ClientSecret,
[Parameter(Mandatory=$true)]
[string]$RedirectUri,
[Parameter(Mandatory=$true)]
[string]$Resource,
[Parameter(Mandatory=$false)]
[string]$Scope,
[Parameter(Mandatory=$false)]
[switch]$AdminConsent,
[Parameter(Mandatory=$false)]
[switch]$Cache,
[Parameter(Mandatory=$false)]
[ValidateSet("Yes","No","Force")]
[ValidateNotNullOrEmpty()]
[string]$Refresh = "Yes"
)
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Web
$isCache = $Cache.IsPresent
$isRefresh = (($Refresh -eq "Yes") -or ($Refresh -eq "Force"))
$refreshForce = $Refresh -eq "Force"
if ($isRefresh)
{
$isCache = $true
}
# Don't edit variables below (unless there's a bug)
$clientSecretEncoded = [uri]::EscapeDataString($clientSecret)
$redirectUriEncoded = [uri]::EscapeDataString($redirectUri)
$resourceEncoded = [uri]::EscapeDataString($resource)
$accessTokenUrl = "https://login.microsoftonline.com/common/oauth2/token"
$cacheFilePath = [System.IO.Path]::Combine($env:TEMP, "aad_web_cache_$clientId.json")
$accessTokenResult = $null
$adminConsentText =""
if ($adminConsent)
{
$adminConsentText = "&prompt=admin_consent"
}
$authorizationUrl = "https://login.microsoftonline.com/common/oauth2/authorize?resource=$resourceEncoded&client_id=$clientId&scope=$scope&redirect_uri=$redirectUriEncoded&response_type=code$adminConsentText"
function Invoke-OAuth()
{
$Global:authorizationCode = $null
$form = New-Object Windows.Forms.Form
$form.FormBorderStyle = [Windows.Forms.FormBorderStyle]::FixedSingle
$form.Width = 640
$form.Height = 480
$form.MaximizeBox = $false
$form.MinimizeBox = $false
$web = New-Object Windows.Forms.WebBrowser
$form.Controls.Add($web)
$web.Size = $form.ClientSize
$web.DocumentText = "<html><body style='text-align:center;overflow:hidden;background-image:url(https://secure.aadcdn.microsoftonline-p.com/ests/2.1.6856.20/content/images/backgrounds/0.jpg?x=f5a9a9531b8f4bcc86eabb19472d15d5)'><h3 id='title'>Continue with current user or logout?</h3><div><input id='cancel' type='button' value='Continue' /></div><br /><div><input id='logout' type='button' value='Logout' /></div><h5 id='loading' style='display:none'>Working on it...</h5><script type='text/javascript'>var logout = document.getElementById('logout');var cancel = document.getElementById('cancel');function click(element){document.getElementById('title').style.display='none';document.getElementById('loading').style.display='block';logout.style.display='none';cancel.style.display='none';if (this.id === 'logout'){window.location = 'https://login.microsoftonline.com/common/oauth2/logout?post_logout_redirect_uri=' + encodeURIComponent('$authorizationUrl');}else{window.location = '$authorizationUrl';}}logout.onclick = click;cancel.onclick = click;</script></body></html>"
$web.add_DocumentCompleted(
{
$uri = [uri]$redirectUri
$queryString = [System.Web.HttpUtility]::ParseQueryString($_.url.Query)
if($_.url.authority -eq $uri.authority)
{
$authorizationCode = $queryString["code"]
if (![string]::IsNullOrEmpty($authorizationCode))
{
$form.DialogResult = "OK"
$Global:authorizationCode = $authorizationCode
$Global:authorizationCodeTime = [datetime]::Now
}
$form.close()
}
})
$dialogResult = $form.ShowDialog()
if($dialogResult -eq "OK")
{
$authorizationCode = $Global:authorizationCode
$headers = @{"Accept" = "application/json;odata=verbose"}
$body = "client_id=$clientId&client_secret=$clientSecretEncoded&redirect_uri=$redirectUriEncoded&grant_type=authorization_code&code=$authorizationCode"
$accessTokenResult = Invoke-RestMethod -Uri $accessTokenUrl -Method POST -Body $body -Headers $headers
$Global:accessTokenResult = $accessTokenResult
$Global:accessTokenResultTime = [datetime]::Now
$accessTokenResultText = (ConvertTo-Json $accessTokenResult)
if ($isCache -and ![string]::IsNullOrEmpty($accessTokenResultText))
{
[void](Set-Content -Path $cacheFilePath -Value $accessTokenResultText)
}
Write-Output (ConvertTo-Json $accessTokenResultText)
}
$web.Dispose()
$form.Dispose()
}
function Get-CachedAccessTokenResult()
{
if ($isCache -and [System.IO.File]::Exists($cacheFilePath))
{
$accessTokenResultText = Get-Content -Raw $cacheFilePath
if (![string]::IsNullOrEmpty($accessTokenResultText))
{
$accessTokenResult = (ConvertFrom-Json $accessTokenResultText)
if (![string]::IsNullOrEmpty($accessTokenResult.access_token))
{
$Global:accessTokenResult = $accessTokenResult
return $accessTokenResult
}
}
}
return $null
}
function Invoke-Refresh()
{
$refreshToken = $accessTokenResult.refresh_token
$headers = @{"Accept" = "application/json;odata=verbose"}
$body = "client_id=$clientId&client_secret=$clientSecretEncoded&resource=$resourceEncoded&grant_type=refresh_token&refresh_token=$refreshToken"
$accessTokenResult2 = Invoke-RestMethod -Uri $accessTokenUrl -Method POST -Body $body -Headers $headers
$accessTokenResult.scope = $accessTokenResult2.scope
$accessTokenResult.expires_in = $accessTokenResult2.expires_in
$accessTokenResult.ext_expires_in = $accessTokenResult2.ext_expires_in
$accessTokenResult.expires_on = $accessTokenResult2.expires_on
$accessTokenResult.not_before = $accessTokenResult2.not_before
$accessTokenResult.resource = $accessTokenResult2.resource
$accessTokenResult.access_token = $accessTokenResult2.access_token
$accessTokenResult.refresh_token = $accessTokenResult2.refresh_token
$Global:accessTokenResult = $accessTokenResult
$Global:accessTokenResultTime = [datetime]::Now
$accessTokenResultText = (ConvertTo-Json $accessTokenResult)
if (![string]::IsNullOrEmpty($accessTokenResultText))
{
[void](Set-Content -Path $cacheFilePath -Value $accessTokenResultText)
}
Write-Output (ConvertTo-Json $accessTokenResultText)
}
$accessTokenResult = Get-CachedAccessTokenResult
if ($accessTokenResult -eq $null)
{
Invoke-OAuth
}
elseif ($refreshForce -or (([datetime]::Parse("1/1/1970")).AddSeconds([int]$accessTokenResult.expires_on).ToLocalTime() -lt ([datetime]::Now).AddMinutes(5)))
{
if ($isRefresh)
{
Invoke-Refresh
}
else
{
Invoke-OAuth
}
}
else
{
Write-Output (ConvertTo-Json $Global:accessTokenResult)
}
Updated Aug 27, 2020
Version 2.0SPDev_Support
Microsoft
Joined April 30, 2019
Microsoft SharePoint Blog
Welcome to the SharePoint Blog! Learn best practices, news, and trends directly from the SharePoint team.