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
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).
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) }