Ok, I think I've found solution that should work for the cases where you are administrator and can give yourself consent. If you can't grant consent I think you'll be stuck for daemon style applications. They would be able to acquire a token, but that token won't have any permissions.
I did some more experimenting and created a new application on my personal tenant where I could give consent and it worked so I can resolve this issue since it was originally about how to get acquire token using client credentials for an application scope. The issue for my case has shifted to now be about consent which is different.
(I'm likely going to delete this app registration and create a new one in different tenant so I don't mind leaving client id guids since they aren't secrets)
Notes:
- The code above is correct so you can copy it and use with your different configuration values (authority, clientId, clientSecret)
- The authority must use the actual tenant id and not "common"
- For client credentials the scope must be full URL and must be in the form: api://7cf8f145-b6e2-403b-9d42-e9f99f864016/.default
There were multiple issues so I will summarize them:
- If you use https://login.microsoftonline.com/common in your authority (the URL you would use for delegated user access) but clientId correct the error message is: "Specified tenant identifier '7cf8f145-b6e2-403b-9d42-e9f99f864016' is neither a valid DNS name, nor a valid external domain." This was confusing to me because it seems to be using the client id as the tenant id, which did not make sense.
2. The scope must be form of api://<resourceId>/.default
This was the issue that spend the most time on.
When using delegated permission, I would use the URL from this area in Azure Portal as the value for the scope
Then when creating new Application permission I thought URL in similar position was also the scope
I then saw the documentation say "scope={resource}/.default" and thought the /.default was simply ADDED to existing resource.
This would make it: Incorrect: api://7cf8f145-b6e2-403b-9d42-e9f99f864016/Task.Write/.default
The /.default actually REPLACES the existing name Correct: api://7cf8f145-b6e2-403b-9d42-e9f99f864016/.default
When you acquire token with delegated user permissions, such as to graph api, the scope will be added to the token with scp claim, resulting in something like this:
"scp": "openid profile User.Read email",
When you acquire token using app permission the roles will be added to the token in roles claim resulting in something like this:
{
....
"roles": [
"Task.Write",
],
...
}
Feedback:
- There are some improvements that could be made to the UI and Azure portal to make thing clearer
- The error message when you have common in authority but attempting client credentials grant could be clearer
- I think the API can be confusing because values used for certain terms is not consistent For example, when specifying scopes I had done "user" authentication before and scopes requested were always short strings like "openid profile User.Read email" However, when the same term "scope" is used event for ConfidentialClient where the value you put is actually the entire resource identifier URI
- The weird quirk about using /.default seems unnecessary. If this is truly a default then why isn't it assumed by AAD and remove the complexity from user / config.