Continuing on with what I was doing earlier about authenticated access to Azure Functions I started doing the following recently:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
try { $cachedtoken = ConvertFrom-SecureString -SecureString $Global:__TempToken -AsPlainText } catch {} $header = @{ "X-ZUMO-AUTH" = $cachedtoken } try { Invoke-RestMethod -Method POST -Uri $apiUrl -Headers $header -ContentType 'application/json' -Body (ConvertTo-Json $UserObj -Depth 5) } catch { if ($Error[0].ErrorDetails.Message -eq "You do not have permission to view this directory or page.") { Write-Output "Authenticating with Azure AD to access the API." # Get Azure AD token for authentication $aadtoken = Get-MSDeviceToken -TenantId $tenantId -AppId $appId -Scope $scope # Based on https://docs.microsoft.com/en-us/azure/app-service/configure-authentication-customize-sign-in-out#client-directed-sign-in Write-Output "Exchanging Azure AD token from Function token." $response = $null $authBody = @{ "access_token" = $aadtoken.id_token } $response = Invoke-RestMethod -Method POST -Uri $apiAuthUrl -Body (ConvertTo-Json $authBody) -ContentType "application/json" $header = @{ "X-ZUMO-AUTH" = $response.authenticationToken } # Cache the token for use in subsequent invocations of the script [securestring]$Global:__TempToken = ConvertTo-SecureString -AsPlainText -Force -String $response.authenticationToken Invoke-RestMethod -Method POST -Uri $apiUrl -Headers $header -ContentType 'application/json' -Body (ConvertTo-Json $UserObj -Depth 10) } else { Write-Output "Unable to connect to API endpoint " + $Error[0].ErrorDetails.Message } } |
What it does is I first see if I have a global variable called __TempToken
set on my machine and if so pull that and use it to authenticate against my Azure Function. If that fails I then call my Get-MSDeviceToken
function to authenticate with Azure AD, exchange the token for a Function one, and then authenticate with that but also store it in the global variable. It’s just a convenience thing so I could avoid having to authenticate each time I was testing my script against the Function.
I realized that even after a day this cached token is valid. That’s not good. So I checked its claims:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "alg": "HS256", "typ": "JWT" }.{ "stable_sid": "sid:24a31ecf1ed162e7ce7ac5a34bcdfd00", "sub": "sid:0ca6b6db384251ca6b7d675dec7c8ce0", "idp": "aad", "ver": "3", "nbf": 1638292659, "exp": 1640884659, "iat": 1638292659, "iss": "https://function.azurewebsites.net/", "aud": "https://function.azurewebsites.net/" }.[Signature] |
The nbf
claim stands for “not before” – i.e. that starting time of validity. And the exp
claim stands for “expiry” – i.e. ending time. Subtracting the two in my case gives 2592000 seconds – which is 720 hours, or 30 days. Whoa!
So I double checked the nbf
value with an epoch converter site and sure enough it is for 30th Dec.
That makes no sense. Naturally, I Googled and came across this official link on the expiration time of session tokens (session tokens are the ones issued by the Function middleware; access tokens are the ones from your identity provider). This has the following:
The authenticated session expires after 8 hours. After an authenticated session expires, there is a 72-hour grace period by default. Within this grace period, you’re allowed to refresh the session token with App Service without reauthenticating the user.
That doesn’t talk about my issue but it seems an authenticated session expires after 8 hours (this must be to do with cookies?) and you can refresh that for up to 72 hours. I don’t need this, but I tried to reduce the setting from 72 to 2 hours via the command line given there but it threw an error: Operation returned an invalid status 'Bad Request'
Not sure why it was throwing an error, but I decided to not using the command and go via Resource Explorer instead and that worked. It’s under config
> authsettingsV2
and called tokenRefreshExtensionHours
(has a value of 2 below).
As expected this didn’t make a difference to the session token lifetime but I figure I’ll post this workaround for anyone else who’s interested.
I am not sure what to do here so I’ll log an issue on GitHub for the 1 month access token issue I suppose. Thing is the token from Azure AD is valid for only 1 hour – and that’s all I want – so it would be good if I could reduce the validity of these. Meanwhile, this is something to be aware of.
Update: I added the following code in my Function to validate the token. As I mentioned earlier I have some code in the Function to capture the username etc. from the claims so I extended that to check the iat
claim (issued at).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# === Process the access token === # Get the ID Token from the header (called "access_token" for some reason!) $access_token = $Request.Headers.'x-ms-token-aad-access-token' # I am interested in the body, so split it along dots... and the second part is the body (first is header, last is signature) $bodyBase64 = ($access_token -split '\.')[1] # Pad what I got above. Need this hack to workaround this error: # MethodInvocationException: Exception calling "FromBase64String" with "1" argument(s): "The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters." switch ($bodyBase64.Length % 4) { 0 { break } 1 { $bodyBase64 += '===' } 2 { $bodyBase64 += '==' } 3 { $bodyBase64 += '=' } } # Convert the Base64 to get JSON; then convert this JSON to as HashTable $claims = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($bodyBase64)) | ConvertFrom-Json -Depth 10 # If someone tries to authenticate with a cached token check if it is older than an hour. # The Azure AD ID Token has an expiry of 1 hour but the Function middleware token has an expiry of 30 days so I want to avoid anyone caching that token and bypassing authentication. # So I make use of the iat claim - which stands for issued at. This is in epoch time (seconds since 1st Jan 1970). Could have used nbf also which is similar (not before). # I convert the current time to epoch time, subtract from iat flaim, and error out if greater than 3600 seconds (1 hour). if ([int](New-TimeSpan -Start (Get-Date "01/01/1970") -End (Get-Date)).TotalSeconds - $claims.iat -gt 3600) { $body = "You do not have permission to view this directory or page." Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::Unauthorized Body = $body }) exit } # Continue with the rest of the script... |