Azure AD Access Token Lifetimes and Long-running PowerShell Scripts

Sometimes Scripts Need Extended Azure AD Access Token Lifetimes

A recent issue where Microsoft limited the page size for the Graph List Users API when retrieving sign-in activity sparked a request from a reader who had problems with a script. They reported that roughly an hour into the script, it failed with a 401 Unauthorized error. The reason is that the access token granted to the app to allow it to run Graph requests to fetch data expired, meaning that the next time the app tried to request data, the Graph refused.

The default Azure AD access token lifetime varies between 60 and 90 minutes (75 minutes on average). The variation exists on purpose to avoid cyclical spikes in demand. Exceptions to the rule do exist. For example, applications like SharePoint Online and OWA that support continuous access evaluation (CAE) can use tokens that last up to 28 hours. These apps support a feature known as claim challenge that is unlikely to be found in apps that execute Graph requests through PowerShell.

Apps can retrieve access tokens from Azure AD using different OAuth 2.0 authentication flows, including password, device code, and authorization code. Azure AD registered apps usually use the client credentials authentication flow. The app authenticates using its own credentials instead of trying to impersonate a user. Valid app credentials include a secret known to the app, a certificate, or a certificate thumbprint.

The client credentials authentication flow does not include the issuance of a refresh token. The lack of a refresh token, which allows apps to silently renew access tokens, means that if you want to keep a script running, you must either:

  • Configure the tenant with a longer access token lifetime.
  • Include code in the script to fetch a new access token before the current one expires.

Configurable Azure AD Access Token Lifetimes

Azure AD supports configurable token lifetimes. This is a preview feature that can set a longer lifetime for an access token. However, the current implementation supports setting token lifetimes for all apps in an organization or for multi-tenant applications. For instance, this code creates a new token lifetime policy that sets a default two-hour token lifetime. Note the organization default setting is True, so this policy applies to all apps in the organization.

$PolicySettings = @{
    "definition"= @("{'TokenLifetimePolicy':{'Version': 1, 'AccessTokenLifetime': '2:00:00'}}")
    "displayName"= "Org-wide 2 Hr AccessTokenPolicy"
    "IsOrganizationDefault" = $True
} 
 
New-MgPolicyTokenLifetimePolicy -BodyParameter $PolicySettings

To test the policy, use an app to request an access token. Here is some PowerShell code to get an access token using the client credentials authentication flow. In this case, the credential is a client secret stored in the app.

$AppId = “de0d7a5d-982a-49e2-8c52-f4596f32b437”
$TenantId = “a662313f-14fc-43a2-9a7a-d2e27f4f3478”
$AppSecret = “3il8Q~Yx4_DOJZxHAxvp7akxW5TQxXdSzhsGpdme”
$Uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$Body = @{
    client_id     = $AppId
    scope         = "https://graph.microsoft.com/.default"
    client_secret = $AppSecret
    grant_type    = "client_credentials"
}
# Get OAuth 2.0 Token
$TokenRequest = Invoke-WebRequest -Method Post -Uri $Uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
# Unpack Access Token
$Token = ($tokenRequest.Content | ConvertFrom-Json).access_token

Write-Host ("Retrieved new access token at {0}" -f (Get-Date)) -foregroundcolor red

Take the access token stored in the $Token variable and examine its contents. For example, Figure 1 shows how jwt.io displays the settings in an access token. To verify that the token lifetime works is expected, compare the time of issuance with the expiration time. In this instance, the timespan should be two hours.

Checking the expiration time for an Azure AD access token

Azure AD access token lifetime
Figure 1: Checking the expiration time for an Azure AD access token

Although creating a token lifetime policy with a new default lifetime for the organization works, increasing token lifetime in this manner is not something to do on a whim. It would be better to be able to assign a token lifetime policy only to the apps that need to use extended token lifetimes.

An organization can support multiple token lifetime policies. It would be nice to be able to apply suitable policies to apps as needed but this doesn’t seem to be possible currently. The Microsoft PowerShell Graph SDK includes the New-MgApplicationTokenLifetimePolicyByRef cmdlet, and you can use the cmdlet assign a token lifetime policy to an application. Alas, this has no effect on the access tokens issued by Azure AD to the app. I’ve been discussing this point with Microsoft and investigations continue.

Tracking Azure AD Access Token Lifetime in Scripts

The alternative is to incorporate code into scripts to track the lifetime of an access token so that the script can retrieve a new token before the old one expires. The script for the Microsoft 365 Groups and Teams Activity Report uses this technique. A function checks if the current time is greater than the calculated token expiration time. If it is, the script requests a new token:

Function Check-AccessToken {
# Function to check if the access token needs to be refreshed. If it does, request a new token
# This often needs to happen when the script processes more than a few thousands groups
$TimeNow = (Get-Date)
if($TimeNow -ge $TokenExpiredDate) {
  $Global:Token = GetAccessToken
  $Global:TokenExpiredDate = (Get-Date).AddMinutes($TimeToRefreshToken) 
#  Write-Host "Requested new access token - expiration at" $TokenExpiredDate 
}
Return $Token
}

The function can then be called whenever necessary within the script.

$Global:Token = Check-AccessToken

This is a relatively unsophisticated mechanism, but it allows the script to process tens of thousands of groups. Variations on the theme can handle other situations.

Only for Special Scripts

The default lifetime for an access token is sufficient for most scripts. Even scripts that run dozens of Graph requests can usually complete processing in a few minutes. It is scripts that must retrieve tens of thousands of items (or even hundreds of thousands of items) that usually deal with inadequate Azure AD access token lifetimes. In those cases, you’ll be glad that methods exist to avoid the dreaded 401 Unauthorized error.


Support the work of the Office 365 for IT Pros team by subscribing to the Office 365 for IT Pros eBook. Your support pays for the time we need to track, analyze, and document the changing world of Microsoft 365 and Office 365.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.