Just a post to myself on the Issuer (iss
) claim in Azure AD tokens. Got confused today with these and this is an attempt to make note of them in one place.
I am talking about Azure AD tokens in the context of authentication to Azure Functions via Azure AD. There are numerous posts on that topic in this blog. In this first post I examined an ID token and its iss
claim is “https://login.microsoftonline.com/<tenantId>/v2.0
“.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ "typ": "JWT", "alg": "RS256", "kid": "l35Q-50cCH4xBVZLHTGwnSR7680" }.{ "aud": "4b2a108b-829c-4c56-889a-30cd226dfb6b", "iss": "https://login.microsoftonline.com/xxxxxx/v2.0", "iat": 1637837091, "nbf": 1637837091, "exp": 1637840991, "aio": "AVQAq/8TAAAANSQQ/rpVIlXZf7TT5zdHNfvQu8V1t/xVrkBS+M9oZd5mNYhL+3cmuM1yOOosGtZuzp6sN1INRBgEdy95ODiWa1ziLGbti1PC/DoYQL6MPqQ=", "rh": "0.ARsAEbFJPNsZjUWD_xrwrJrjW4sQKkucglZMiJowzCJt-2obADU.", "sub": "A1w9raaA8r-C5LwmKNYAXc_IiRFRaWB2VOTpSgUfsDo", "tid": "xxxxxx", "uti": "qOrkOFFjTk-irWB3d8niAA", "ver": "2.0" }.[Signature] |
Unfortunately I didn’t take a screenshot of what is setup on the Azure Function side, but looking at the docs today it looks like the Issuer must be similar to the above – i.e. “https://login.microsoftonline.com/<tenantId>/v2.0
“. The only difference being to omit “/v2.0
” in case of Azure AD v1 endpoints.
Am guessing that’s what I had as standard on my Azure Function too, but I am not sure*. Because in a later post where I authenticate to the Function via client credentials grant, I have a screenshot where on the Azure Function side I have “https://sts.windows.net/<tenantId>/v2.0
” instead. Moreover, in that instance the token I was examining had the iss
claim as “https://sts.windows.net/<tenantId>/
” and I had even changed it on Azure Functions to remove “/v2.0
“.
(*Actually, scratch that. While writing this post I created a new Azure Function and set it up to use Azure AD. The Issuer Url of that is “https://sts.windows.net/<tenantId>/v2.0
” – so the MS doc is incorrect).
What is the difference between these two URLs btw? The “sts.windows.net
” iss
claim is returned when using a v1.0 endpoint; the “login.microsoftonline.com
” endpoint is used with a v2.0 endpoint. I talk about this in a prior blog post too. From this GitHub thread that I linked there too:
From another doc, on App Registrations, the accessTokenAcceptedVersion
property in the App Registration manifest “specifies the access token version expected by the resource” (so… Azure Functions, in my case?). To quote further: “The endpoint used, v1.0 or v2.0, is chosen by the client and only impacts the version of id_tokens.”
Possible values for this property are 1, 2, or null. 2 corresponds to a v2.0 endpoint; while 1 and null correspond to a v1.0 endpoint. By default the value is null, so the App Registration returns v1.0 tokens I suppose? I say “I suppose” because if I try to get an ID token via the ROPC or Device Code flow I always get the Issuer as “login.microsoftonline.com
” and not “sts.windows.net
” – which makes no sense. Interestingly, in both flows the Access Token I get have the Issuer as “sts.windows.net”.
This leads me to think I must have got my understanding of the accessTokenAcceptedVersion
property wrong.
I learnt that one can figure out the version of the endpoint from the “ver
” claim in the token too. Based on this it looks like ID tokens are v2.0 and hence use “login.microsoftonline.com
” while access tokens are v1.0. So maybe the document is incorrect in saying that the accessTokenAcceptedVersion
property only affects ID tokens; instead it affects Access Tokens just like its name says.
Moving on…
I am pretty sure that even with the Issuer being “login.microsoftonline.com
” in the ID tokens I get, and Azure Functions set to “sts.windows.net
“, it has worked so far for me. The only time it didn’t work was when I used the Client Credentials grant flow as mentioned earlier because of the extra “/v2.0
” in the Azure Function Issuer URL that the Access Token did not have – and that makes sense because this flow returns Access Tokens, as like I noted with the other two flows above too Access Tokens seem to have issuer as “sts.windows.net”.
To recap:
- By default Azure Function + Azure AD has an Issuer URL set to “
https://sts.windows.net/<tenantId>/v2.0
“ - ROPC and Device Code flow ID tokens have the Issuer URL claim set to “
https://login.microsoftonline.com/<tenantId>/v2.0
“. The same in Access tokens is “https://sts.windows.net/<tenantId>/
“ - Client Credential flow doesn’t return ID tokens so Issuer URL is moot there; it returns Access tokens and they too have “
https://sts.windows.net/<tenantId>/
“ - To get Client Credential flow working I have to change the Issuer URL from “
https://sts.windows.net/<tenantId>/v2.0
” to “https://sts.windows.net/<tenantId>/
” on the Azure Function side. - I am not sure what Issuer URL is present in the claims when a Logic App calls an Azure Function via Azure AD authentication – Access tokens, if I had to guess – but it works fine with either “
sts.windows.net
” variant on the Azure Function side.
Here’s something interesting. While the Azure Function had its Issuer URL set to “https://sts.windows.net/<tenantId>/v2.0
” I was able to authenticate against it via the ROPC and Device Code flows even though their issuer URL was “https://login.microsoftonline.com/<tenantId>/v2.0
“. When I changed the Issuer URL (in Azure Functions) to remove the “/v2.0”, that broke the ROPC and Device Code flows.
That led me to experimenting with the Issuer URL on the Azure Function side by changing it from “https://sts.windows.net/<tenantId>/
” to “https://login.microsoftonline.com/<tenantId>/"
and testing the Client Credential flow. I expected it to fail, but it worked fine. Adding a “/v2.0” back broke it again, though.
To summarize:
- “
https://sts.windows.net/<tenantId>/v2.0
” and “https://login.microsoftonline.com/<tenantId>/v2.0
” seem to be equivalent. - Issuer URL of a newly created Azure Function setup with Azure AD authentication is “
https://sts.windows.net/<tenantId>/v2.0
“ - “
https://sts.windows.net/<tenantId>/
” and “https://login.microsoftonline.com/<tenantId>/
” seem to be equivalent. - What seems to matter is the “
/v2.0
” – ROPC and Device Code flow ID tokens have this as Issuer; Client Credential flow Access tokens don’t. - A Logic App connecting to Azure Function using via Azure AD authentication (using a Managed Identity) seems to be ok either ways.
So what do I do if I want to open up authenticated Access to an Azure Function and want all three flows to work?
To begin with, using the “/v2.0” Issuer seems to be the sensible idea – assuming it means we are using the v2 endpoint. So I should add that back on the Azure Function side (which is the default anyways) and look to what I can do so that the Client Credential flow access token too is from a v2.0 endpoint.
I know that the accessTokenAcceptedVersion
property can be used to get a token from the v2.0 endpoint. It seems to be for ID tokens only, while I am interested in Access tokens (as its a Client Credential flow) but assuming the doc is wrong it’s worth changing this to see if I can get Access Tokens from the v2.0 endpoint. This way I can use the “/v2.0” Issuer URL with all three flows.
When I use Client Credentials I am doing what I blogged about in a prior post. I have App Registrations for each “client”. They have permissions (via App Roles) to the App Registration tied to the Azure Function. So what I should probably do is change the accessTokenAcceptedVersion
property on the App Registration tied to the Azure Function? Not sure, but that’s what I am going to try. Weird thing is this is what the Logic App Managed Identity too has permissions to (via App Roles), and it works already, so I hope it doesn’t break anything.
…
The change took effect instantly. And voila! whereas previously I was getting an Access Token like this:
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 |
{ "typ": "JWT", "alg": "RS256", "x5t": "2ZQpJ3UpbjAYXYGaXEJl8lV0TOI", "kid": "2ZQpJ3UpbjAYXYGaXEJl8lV0TOI" }.{ "aud": "api://7a01aae6-12c9-2aa1-1446-918d456f065a", "iss": "https://sts.windows.net/1b29b111-29db-258d-73ff-2af0ac9ae35a/", "iat": 1666656690, "nbf": 1666656690, "exp": 1666660590, "aio": "F2ZaYHD9N/XE5ot5He2iV1Z1VfAyAAB=", "appid": "fd8289ec-657f-4c8f-9fb8-fe23b7cc3ae3", "appidacr": "1", "idp": "https://sts.windows.net/1b29b111-29db-258d-73ff-2af0ac9ae35a/", "oid": "f9050bef-c1c0-4569-8f82-cd98347ab5aa", "rh": "0.ARsAEbFJPNsZjUWD_xrwrJrjW-aqAWHJZaFKpEaRjUVvBlMbAAA.", "roles": [ "My.App" ], "sub": "f9050bef-c1c0-4569-8f82-cd98347ab5aa", "tid": "1b29b111-29db-258d-73ff-2af0ac9ae35a", "uti": "mPAUgoBlmECyUsTqtC4CAA", "ver": "1.0" }.[Signature] |
Now I get this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
{ "typ": "JWT", "alg": "RS256", "kid": "2ZQpJ3UpbjAYXYGaXEJl8lV0TOI" }.{ "aud": "7a01aae6-12c9-2aa1-1446-918d456f065a", "iss": "https://login.microsoftonline.com/1b29b111-29db-258d-73ff-2af0ac9ae35a/v2.0", "iat": 1666731403, "nbf": 1666731403, "exp": 1666735303, "aio": "E2ZgYNB983ZVwM2e288nMmhe5ngeDQA=", "azp": "fd8289ec-657f-4c8f-9fb8-fe23b7cc3ae3", "azpacr": "1", "oid": "f9050bef-c1c0-4569-8f82-cd98347ab5aa", "rh": "0.ARsAEbFJPNsZjUWD_xrwrJrjW-aqAWHJZaFKpEaRjUVvBlMbAAA.", "roles": [ "My.App" ], "sub": "f9050bef-c1c0-4569-8f82-cd98347ab5aa", "tid": "1b29b111-29db-258d-73ff-2af0ac9ae35a", "uti": "J5vcUhOg1UCpVjnAYsJcAA", "ver": "2.0" }.[Signature] |
The new token is indeed v2.0 (“ver
” claim). And it has the “/v2.0
” Issuer URL (“iss
” claim). Slight differences – in the v1.0 token the audience (“aud
” claim) was of the form “api://<appId>
” whereas now its just “<appId>
” (this <appId>
is that of the one tied to the Azure Function by the way). And more importantly, previously the App Id of the App Registration itself (that of the client who is doing the Client Credential flow) was the “appid
” claim but now its “azp
“. This is confirmed by an MS doc too that says “azp
” is a replacement for “appid
” and only present in v2.0 tokens while “appid
” is only in v1.0 tokens.
The Logic App continues to work as before, so no unexpected issues there either.
So, to re-summarize:
- “
https://sts.windows.net/<tenantId>/v2.0
” and “https://login.microsoftonline.com/<tenantId>/v2.0
” seem to be equivalent Issuer URL endpoints. - “
https://sts.windows.net/<tenantId>/
” and “https://login.microsoftonline.com/<tenantId>/
” seem to be equivalent. - What seems to matter is the “
/v2.0
” – ROPC and Device Code flow ID tokens have this as Issuer; Client Credential flow Access tokens don’t. This is because ID tokens seem to default to the v2.0 endpoint while Access tokens default to the v1.0 endpoint (so the type of token is what seems to change things, rather than ROPC/ Device Code vs Client Credentials; Access Tokens got via the former flow are v1.0 instead of v2.0) - A Logic App connecting to Azure Function using via Azure AD authentication (using a Managed Identity) seems to be ok either ways. I think that’s because Managed Identities don’t use tokens; on the Function side I don’t see any tokens when authenticating with the Managed Identity of a Logic App, instead the headers have ‘
x-ms-workflow-name
‘ (only for Logic Apps) and ‘x-ms-client-principal-id
‘ (for both Logic Apps and if I were to invoke one Azure Function from another via Managed Identity) values which gives the impression its some other mechanism. - Issuer URL of a newly created Azure Function setup with Azure AD authentication is “
https://sts.windows.net/<tenantId>/v2.0
“. It’s fine to leave it as is – but you got to take care if using Client Credential flow (see next point). - The App Registration against which the “client” using Client Credential flow authenticates must have its
accessTokenAcceptedVersion
property in the manifest set to 2. This will result in the Access Token using the v2.0 endpoint. In general this is what one needs to do if Access Tokens must be got from the v2.0 endpoint (as ID tokens seem to default to v2.0 anyways).
And that’s all!
This post/ investigation began as an itch I wanted to scratch as I was getting a bit over my head when making some changes yesterday. I sat up way past my usual bedtime trying to figure this out yesterday; and now its way past bedtime of the next day and I have finally cracked it. Yay!
Lastly, here’s an excerpt from some code I have been developing through these series of posts of authenticating to Azure Functions. I keep modifying it as I discover new edge cases, and variations of this are in other posts as snippets.
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
param($Request, $TriggerMetadata) # === 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' # Check if we have an access token. If we don't have one and the request came through, then it came via the Logic App or Function if ($access_token.Length -ne 0) { # 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 # Store the details in a hashtable $requestor = @{ "name" = $claims.name; "email" = $claims.preferred_username; "oid" = $claims.oid; "appid" = switch ($claims.ver) { "1.0" { $claims.appid } "2.0" { $claims.azp } default { "unknown" } } } # 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 7200 seconds (2 hour). # Originally I used to do 1 hour here, but that started giving errors. I think it's due to DST. So I made this 2 hours to be safe. if ([int](New-TimeSpan -Start (Get-Date "01/01/1970") -End (Get-Date).ToUniversalTime()).TotalSeconds - $claims.iat -gt 7200) { if ($requestor.email.Length -ne 0) { Write-Host "Token has expired. Unauthorized request from " $requestor.email } else { Write-Host "Token has expired. Unauthorized request from " $requestor.appid } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::Unauthorized Body = "You do not have permission to view this directory or page." }) # This is the only instance where I don't return a JSON object. That's because I want to keep it consistent with how an Azure Function typically responds. exit } # Note to self: if you want to convert from epoch: (([System.DateTimeOffset]::FromUnixTimeSeconds($unixTime)).DateTime.ToLocalTime()).ToString("s") # When outputting, I put the email or app Id depending on what I have if ($requestor.email.Length -ne 0) { Write-Host "The Function processed a request from" $requestor.email } else { Write-Host "The Function processed a request from" $requestor.appid } } else { # If there was no access token then chances are this came in via the Logic App. Output its ID for reference: $requestorAppName = $Request.Headers.'x-ms-workflow-name' $requestorAppId = $Request.Headers.'x-ms-client-principal-id' if ($requestorAppName.Length -ne 0) { Write-Host "Function processed a request from Logic App $requestorAppName ($requestorAppId)" } else { Write-Host "Function processed a request from non Logic App (id $requestorAppId)" } } |