Azure Active Directory application model
- 1/22/2016
- The building blocks: Application and ServicePrincipal
- Consent and delegated permissions
- App user assignment, app permissions, and app roles
- Groups
- Summary
Consent and delegated permissions
Now that you know what application aspects are defined in the Application and ServicePrincipal objects, it’s time to understand how these two entities are used in the application provisioning and consent flows.
You have learned that all it takes for provisioning an app in a tenant (creating a ServicePrincipal for that app in the tenant) is one user requesting a token by using the app coordinates defined in the Application object, successfully authenticating, and granting to the app consent to the permissions it requires. To get to the next level of detail, you must take into account what kind of user created the application in the first place, what permissions the applications requires, and what kind of user actually grants consent to the app and in what terms. There is an underlying rule governing the entire process, but that’s pretty complicated. Instead of enunciating it here and letting you wrestle with it, I am going to walk you through various common scenarios and explain what happens. Little by little, we’ll figure out how things work.
Initially, I’ll scope things down to the case in which you are creating line-of-business apps—applications meant to be consumed by users from the same directory in which they were created. If your company has an IT department that creates apps for your company’s employees, you know what kind of apps I am referring to. Once you have a solid understanding of how consent works within a single directory, I’ll venture to the multitenant case, where you’ll see more of the provisioning aspect. I’ll stick to delegated permissions, but there are other kinds of permissions, like the things that an app can do independently of which user is signed in at the moment, but I’ll defer coverage of those and describe the basics here.
Application created by a nonadmin user
In Chapter 5 you followed instructions to create an application in Azure AD via the Azure portal. Did you create it while being signed in with a user who is a global administrator in your tenant? If not, that’s perfect—the app you have is already in the state I’ll describe in this section. If you did, you can choose to believe that my description is accurate—or you can create a new app, following the same instructions (very important!) but using a nonadmin user. Note: to be able to sign in with that account in the Azure portal, you might need to promote that user to coadmin of your Azure subscription.
As you have seen in the preceding section, creating one app via the Azure portal has the effect of creating both the Application object and the corresponding ServicePrincipal. What you haven’t seen yet is how the directory remembers what permissions have been granted to the ServicePrincipal and to which user. The Application object enumerates the permissions it needs in the requiredResourceAccess collection, but those aren’t present in the ServicePrincipal. Where are they?
Azure AD maintains another collection of entities, named oauth2PermissionGrants, which records which clients have access to which resources and with what permissions. Critically, oauth2PermissionGrants also records which users that consent is valid for.
For example, when you created the sample app in the Azure portal, Azure AD automatically granted consent for that app on behalf of your user. Alongside the Application and ServicePrincipal, the process also created the following oauth2PermissionGrants entry:
{ "odata.metadata": "https://graph.windows.net/developertenant.onmicrosoft.com/$metadata#oauth2 PermissionGrants", "value": [ { "clientId": "29f565fd-0889-43ff-aa7f-3e7c37fd95b4", "consentType": "Principal", "expiryTime": "2015-11-21T23:31:32.6645924", "objectId": "_WX1KYkI_0Oqfz58N_2VtEnIMYJNhOpOkFrsIuF86Y8", "principalId": "13d3104a-6891-45d2-a4be-82581a8e465b", "resourceId": "8231c849-844d-4eea-905a-ec22e17ce98f", "scope": "UserProfile.Read", "startTime": "0001-01-01T00:00:00" } ] }
Let’s translate that snippet into English. It says that the User with identifier 13d3104a-6891-45d2-a4be-82581a8e465b (the PrincipalId) consented for the client 29f565fd-0889-43ff-aa7f-3e7c37fd95b4 (the clientId) to access the resource 8231c849-844d-4eea-905a-ec22e17ce98f (the resourceId) with permission UserProfile.Read (the scope). Resolving references further, the client is our sample app, and the resource is the directory itself—more precisely, the Directory Graph API. Figure 8-3 shows how the consent for the first application user is recorded in the directory; Figure 8-4 shows how the oauth2PermissionGrants table grows as more users give their consent.
Figure 8-3 The oauth2PermissionGrant recording in the directory that user 1 consented for the app represented by ServicePrincipal 1 to access ServicePrincipal N with the permission stored in the property scope, in itself picked from one of the permissions exposed by the original Application N oauth2Permissions section.
Figure 8-4 Subsequent consent operations create more oauth2PermissionGrant entries in the directory, one for each new user consenting for the application.
When Azure AD receives a request for a token to be issued to the application defined here, it looks in the oauth2PermissionGrants collection for entries whose clientId matches the app. If the authenticated user has a corresponding entry, she or he will get back a token right away. If there’s no entry, the user will see the consent prompt listing all the requiredResourceAccess permissions from the Application object. Upon successful consent, a new oauth2PermissionGrant entry for the current user will be created to record the consent. And so on and so forth.
If you want to try, go ahead and launch the sample app again, but sign in as another user. This time, you will be presented with the consent page. Consent and then sign out. Sign in again with the new user: you will not be prompted for consent again. If you queried the directory (in the next chapter you’ll learn how) to find all the oauth2PermissionGrants whose clientId matches the sample app, you’d see that there are now two entries, looking very much alike apart from the principalId, which would point to different users. Note that it doesn’t matter whether your second user is an administrator or a low-privilege user; the resulting oauth2PermissionGrant will look just like the one described earlier when following this flow.
Interlude: Delegated permissions to access the directory
One of the things you have learned in this chapter is that applications can declare the permissions that a client can request of them, via oauth2Permissions, as a way of partitioning the possible actions a user can perform over the resource represented by the app and to provide fine-grained access control over who can do what. As I’ve mentioned, in the next chapter you will learn how clients can actually take advantage of gaining such permissions; here, you’re just studying how requesting and granting such permissions takes place.
Each and every resource protected by Azure AD works by exposing permissions—the Office 365 API, Azure management API, and any custom API all work that way. Covering all those would be a pretty hard task. Even ignoring the enormous surface I’d have to cover, chances are that the details would change multiple times from the time I’m writing and when you have this book in your hands. That said, I am going to describe in detail at least one resource: the directory itself. Like any other resource, Azure AD exposes a number of delegated permissions, which determine what actions your application is allowed to perform against the data stored in the directory. Such actions take the form of requests to embed information in issued tokens (what we have been working with until now) and reading or modifying directory data via API calls to the Graph API (what you’ll see in the next chapter). You will likely have to deal with directory permissions in practically every app you write; hence, they’re a great candidate for showing you how to deal with permissions in depth—well, except for the fact that they feature lots of exceptions, but you need to be aware of these anyway.
As of today, the directory itself is represented by a ServicePrincipal in your tenant. You already know both the AppId and the ObjectId of that principal, given that our sample app had to request at least the permission UserProfile.Read in order to sign users in. The AppId, 00000002-0000-0000-c000-000000000000, comes from the requiredResourceAccess in the Application object representing our sample. The ObjectID of the ServicePrincipal, 8231c849-844d-4eea-905a-ec22e17ce98f, comes from the oauth2PermissionGrant tracking the consent to our sample. The objectId is enough for crafting the resource URL referring to the Graph API ServicePrincipal: it’s https://graph.windows.net/developertenant.onmicrosoft.com/servicePrincipals/8231c849-844d-4eea-905a-ec22e17ce98f.
I won’t show the entire JSON for the ServicePrincipal here, as it contains a lot of stuff I want to cover later. But take a look at the oauth2Permissions, the collection of delegated permissions one client can request for interacting with the directory:
"oauth2Permissions": [ { "adminConsentDescription": "Allows the app to create groups on behalf of the signed-in user and read all group properties and memberships. Additionally, this allows the app to update group properties and memberships for the groups the signed-in user owns.", "adminConsentDisplayName": "Read and write all groups", "id": "970d6fa6-214a-4a9b-8513-08fad511e2fd", "isEnabled": true, "type": "User", "userConsentDescription": "Allows the app to create groups on your behalf and read all group properties and memberships. Additionally, this allows the app to update group properties and memberships for groups you own.", "userConsentDisplayName": "Read and write all groups", "value": "Group.ReadWrite.All" }, { "adminConsentDescription": "Allows the app to read basic group properties and memberships on behalf of the signed-in user.", "adminConsentDisplayName": "Read all groups", "id": "6234d376-f627-4f0f-90e0-dff25c5211a3" "isEnabled": true, "type": "User", "userConsentDescription": "Allows the app to read all group properties and memberships on your behalf.", "userConsentDisplayName": "Read all groups", "value": "Group.Read.All" }, { "adminConsentDescription": "Allows the app to read and write data in your company or school directory, such as users and groups. Does not allow user or group deletion.", "adminConsentDisplayName": "Read and write directory data", "id": "78c8a3c8-a07e-4b9e-af1b-b5ccab50a175", "isEnabled": true, "type": "Admin", "userConsentDescription": "Allows the app to read and write data in your company or school directory, such as other users, groups. Does not allow user or group deletion on your behalf.", "userConsentDisplayName": "Read and write directory data", "value": "Directory.Write" }, { "adminConsentDescription": "Allows the app to have the same access to information in the directory as the signed-in user.", "adminConsentDisplayName": "Access the directory as the signed-in user", "id": "a42657d6-7f20-40e3-b6f0-cee03008a62a", "isEnabled": true, "type": "User", "userConsentDescription": "Allows the app to have the same access to information in your work or school directory as you do.", "userConsentDisplayName": "Access the directory as you", "value": "user_impersonation" }, { "adminConsentDescription": "Allows the app to read data in your company or school directory, such as users, groups, and apps.", "adminConsentDisplayName": "Read directory data", "id": "5778995a-e1bf-45b8-affa-663a9f3f4d04", "isEnabled": true, "type": "Admin", "userConsentDescription": "Allows the app to read data in your company or school directory, such as other users, groups, and apps.", "userConsentDisplayName": "Read directory data", "value": "Directory.Read" }, { "adminConsentDescription": "Allows the app to read the full set of profile properties of all users in your company or school, on behalf of the signed-in user. Additionally, this allows the app to read the profiles of the signed-in user's reports and manager.", "adminConsentDisplayName": "Read all users' full profiles", "id": "c582532d-9d9e-43bd-a97c-2667a28ce295", "isEnabled": true, "type": "Admin", "userConsentDescription": "Allows the app to read the full set of profile properties of all users in your company or school on your behalf. Additionally, this allows the app to read the profiles of your reports and manager.", "userConsentDisplayName": "Read all users' full profiles", "value": "User.Read.All" }, { "adminConsentDescription": "Allows the app to read a basic set of profile properties of all users in your company or school on behalf of the signed-in user. Includes display name, first and last name, photo, and email address. Additionally, this allows the app to read basic info about the signed-in user's reports and manager.", "adminConsentDisplayName": "Read all users' basic profiles", "id": "cba73afc-7f69-4d86-8450-4978e04ecd1a", "isEnabled": true, "type": "User", "userConsentDescription": "Allows the app to read a basic set of profile properties of other users in your company or school on your behalf. Includes display name, first and last name, photo, and email address. Additionally, this allows the app to read basic info about your reports and manager.", "userConsentDisplayName": "Read all user's basic profiles", "value": "User.ReadBasic.All" }, { "adminConsentDescription": "Allows users to sign in to the app, and allows the app to read the profile of signed-in users. It also allows the app to read basic company information of signed-in users.", "adminConsentDisplayName": "Sign in and read user profile", "id": "311a71cc-e848-46a1-bdf8-97ff7156d8e6", "isEnabled": true, "type": "User", "userConsentDescription": "Allows you to sign in to the app with your work account and let the app read your profile. It also allows the app to read basic company information.", "userConsentDisplayName": "Sign you in and read your profile", "value": "User.Read" } ],
Here’s a quick description of each delegated permission listed, per their Value property. Please note that this list does change over time. Funny story: it changed a couple of weeks after I finished writing this chapter—I had to come back and revise much of what follows. In fact, the change is not fully complete, as the ServicePrincipal object shown above still shows some old values. The first four permissions described in what follows are the ones that Azure AD has offered since it started supporting consent as described in this book; the last four are brand-new and likely to be less stable. Wherever appropriate, I will hint at the old values so that if you encounter code based on older strings, you can map it back to the new permissions. Chances are the list will change again: please keep an eye on the permissions documentation, currently available at https://msdn.microsoft.com/Library/Azure/Ad/Graph/howto/azure-ad-graph-api-permission-scopes.
User.Read (was UserProfile.Read)
This is the permission that each app needs to authenticate users. Applications created in the Azure portal and Visual Studio are configured to automatically request this permission, which is why you don’t see it mentioned in the UI you use for creating apps in either tool.
Besides the ability to request a token containing claims about the incoming user, this permission grants to the app the ability to query the Graph API for information about the currently signed-in user.
As you’ve experienced, this permission can be granted by nonadmin users. That is confirmed by the type property of value User in the permissions declaration.
Directory.Read.All (was Directory.Read)
As the name implies, obtaining this permission allows one application to read via the Graph API (I’ll stop saying that; just assume that’s what you use to interact with the directory) the content of the directory tenant of the user that is currently signed in.
Here’s the first exception. In the general case, Directory.Read is an admin-only permission: only an admin user can consent to it. However, if the application is a web app (as opposed to a native client) defined in tenant A, and the user being prompted for consent is also from A, Directory.Read behaves like a User-type permission, which is to say that even a nonadmin user can consent to it. For the scenario we have been considering until now—app developer and app users are from the same tenant—this is effectively a User-type permission. When we consider the case in which the app is available to other tenants, you’ll see that an app created in A that is requesting Directory.Read and being accessed by a user from B will be provisioned in B only if that user happens to be an administrator.
Directory.ReadWrite.All (was Directory.Write)
Once again, the name is self-explanatory: this permission grants to the app the ability to read, modify, and create directory data. No exceptions this time; only administrator users can consent to Directory.Write.
Directory.AccessAsUser.All (was user_impersonation)
This permission, which today is surfaced in the Azure portal under the label “Access the directory as the signed-in user,” allows the application to impersonate the caller when accessing the directory, inheriting his or her permissions. That is a pretty powerful thing to do, which is why for web applications this permission can be granted only by an admin user.
As a side note, for native applications, this permission behaves like a User permission instead. A native app does not have an identity per se, and it is already doing the direct user’s bidding anyway. It stands to reason that the app should be able to do what the user is able to do, just as happens on-premises when a classic native client (say Word or Excel) can or cannot open a document from a network share depending on whether the user has the correct permissions on that folder.
User.ReadBasic.All
You can think of this permission as the minimum requirement allowing an app to enumerate all users from a tenant. Namely, User.ReadBasic.All will give access to the user attributes displayName, givenName, surname, mail and thumbnailPhoto. Anything beyond that requires higher permissions.
User.Read.All
This is an extension of User.ReadBasic.All. This permission allows an app to access all the attributes of User, the navigation properties manager, and directReports. User.Read.All can be exercised only by admin users.
Group.Read.All, Group.ReadWrite.All
These new permissions are still in preview at this point, so I hesitate to give too detailed a description here. The idea is that groups and group membership are important information and deserve their own permissions so that access can be requested and granted explicitly. Group.Read.All allows an app to read the basic profile attributes of groups and the groups they are a member of. Group.ReadWrite.All allows an app to access the full profile of groups and to change the hierarchy by creating new groups and updating existing ones. Both permissions alone won’t grant access to the users in the groups—to obtain that, the app also needs to request some User.Read* permission.
As usual, it’s important to remember that scopes don’t really add to what a user can do: an application obtaining Group.ReadWrite.All will only be able to manipulate the groups owned by the user granting the delegation to the app.
Table 8-1 summarizes how the out-of-the-box Azure AD permissions work. I’ve added a column for the permission identifier, which I find handy so that when I look at the Application object, which uses only opaque IDs, I know what permission the app is actually requesting. Let me stress that there’s no guarantee these won’t change in the future, so please use them advisedly.
Table 8-1 A summary of the Azure AD permissions for accessing the directory.
Permission description in the Azure portal |
Identifier |
Scope value |
Type |
Sign in and read user profile |
311a71cc-e848-46a1-bdf8-97ff7156d8e6 |
User.Read |
User |
Read directory data |
5778995a-e1bf-45b8-affa-663a9f3f4d04 |
Directory.Read.All |
Admin (except for users from the tenant where the Application is defined) |
Read and write directory data |
78c8a3c8-a07e-4b9e-af1b-b5ccab50a175 |
Directory.ReadWrite.All |
Admin |
Access the directory as the signed-in user |
a42657d6-7f20-40e3-b6f0-cee03008a62a |
Directory.AccessAsUser.All |
Admin (except native clients) |
Read all users’ basic profiles |
cba73afc-7f69-4d86-8450-4978e04ecd1a |
User.ReadBasic.All |
User |
Read all users’ full profiles |
c582532d-9d9e-43bd-a97c-2667a28ce295 |
User.Read.All |
Admin |
Read all groups |
6234d376-f627-4f0f-90e0-dff25c5211a3 |
Group.Read.All |
Admin |
Read and write all groups |
970d6fa6-214a-4a9b-8513-08fad511e2fd |
Group.ReadWrite.All |
Admin |
Now that you have some permissions to play with, let’s get back to the exploration of how consent operates.
Application requesting admin-level permissions
Let’s say that your application needs the ability to modify data in the directory. You might be surprised to learn that you can create such an application even with a nonadmin user: you’ll simply not be able to use it at run time.
Go back to the Azure portal, sign in as a nonadmin user, and go through the usual application creation flow. Once the app is created, head to the Configure tab and scroll all the way to the bottom of the page. As of today, you’ll find a section labeled Permissions To Other Applications, already containing one entry for Azure Active Directory—specifically, the default delegated permission Sign In And Read User Profile. Figure 8-5 shows you the UI at the time of writing, but as usual you can be sure there will be something different (but I hope functionally equivalent) by the time you pick up the book.
Figure 8-5 The application permission selection UI in the Azure portal (fall 2015).
You’ll also see an ominous warning: “You are authorized to select only delegated permissions which have personal scope.” Today that isn’t actually the case. Select Read And Write Directory Data, and then click Save.
You’ll receive a warning that the portal was unable to update the configuration for the app, but that’s only partially true. If you go take a look at the Application, you’ll see that it was correctly updated. Here is its requiredResourceAccess section:
"requiredResourceAccess": [ { "resourceAppId": "00000002-0000-0000-c000-000000000000", "resourceAccess": [ { "id": "78c8a3c8-a07e-4b9e-af1b-b5ccab50a175", "type": "Scope" }, { "id": "311a71cc-e848-46a1-bdf8-97ff7156d8e6", "type": "Scope" } ] }
Thanks to our magical Table 8-1, we know those to be the correct permissions.
The part that the portal was not able to add was the oauth2PermissionGrant that would allow the current (nonadmin) user to have write access to the directory. If you list the oauth2PermissionGrants of the ServicePrincipal, you’ll find only the original entry for User.Read.
That entry is the reason why, if you try to sign in to the app as the user who created it, you will succeed: the directory sees that entry, and that’s enough to not show the consent prompt and issue the requested token. However, if after you sign in, your app attempts to get a token for calling the Graph, the operation would fail.
If you launch the application again and try to sign in as any other nonadmin user, instead of the consent prompt you’ll receive an error along the lines of “AADSTS90093: Calling principal cannot consent due to lack of permissions,” which is exactly what you should expect.
Finally, launch the app again and try to sign in as an administrator. You will be presented with the consent page as in Figure 8-6, just as expected.
Figure 8-6 The consent prompt presented to an admin user.
Grant the consent—you’ll find yourself signed in to the application. That done, take a look at what changed in oauth2PermissionGrants:
{ "odata.metadata": "https://graph.windows.net/developertenant.onmicrosoft.com/$metadata#oauth2 PermissionGrants", "value": [ { "clientId": "725a2d9a-6707-4127-8131-4f9106d771de", "consentType": "Principal", "expiryTime": "2016-02-26T18:17:06.8442687", "objectId": "mi1acgdnJ0GBMU-RBtdx3knIMYJNhOpOkFrsIuF86Y_VUmVPfKg_R6aK4EVKgQSW", "principalId": "4f6552d5-a87c-473f-a68a-e0454a810496", "resourceId": "8231c849-844d-4eea-905a-ec22e17ce98f", "scope": "Directory.Write UserProfile.Read", "startTime": "0001-01-01T00:00:00" }, { "clientId": "725a2d9a-6707-4127-8131-4f9106d771de", "consentType": "Principal", "expiryTime": "2016-02-26T00:50:43.3860871", "objectId": "mi1acgdnJ0GBMU-RBtdx3knIMYJNhOpOkFrsIuF86Y9KENMTkWjSRaS-glgajkZb", "principalId": "13d3104a-6891-45d2-a4be-82581a8e465b", "resourceId": "8231c849-844d-4eea-905a-ec22e17ce98f", "scope": "UserProfile.Read", "startTime": "0001-01-01T00:00:00" } ] }
There’s a new entry now, representing the fact that the admin user consented for the app to have UserProfile.Read and Directory.Write permissions. As discussed earlier, by the time you read this, those scopes will likely have their new values—User.Read and Directory.ReadWrite.All—but it is really exactly the same semantic.
Note that this did not change the access level for anybody but this particular admin user. If you try to sign in as a nonadmin user (other than the app's creator), you’ll still get error AADSTS90093.
Admin consent
If the consent styles you’ve encountered so far were the only ones available, you’d have a couple of serious issues:
- Each and every user, apart from the application developer, would need to consent upon their first use of the app.
- Only admin-level users would be able to consent for applications requiring more advanced access to the directory, even when a user did not plan to exercise those higher privileged capabilities.
Both issues would limit the usefulness of Azure AD. Luckily, there’s a way of consenting to applications that results in a blanket grant to all users of a tenant, all at once, and regardless of the access level requested. That mechanism is known as admin consent, as opposed to user consent, which you’ve been studying so far. Achieving admin consent is just a matter of appending to the request to the authorization endpoint the parameter prompt=admin_consent.
Let’s give it a try and see what happens. From Chapter 7, you now know how to modify authentication requests by adding the change you want to the RedirectToIdentityProvider notification. In a real app, you would add some conditional logic to weave this parameter in only at the time of first access, but for this test you can go with the brute-force solution in which you add it every time.
Here’s the code:
public static Task RedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConn ectMessage, OpenIdConnectAuthenticationOptions> notification) { notification.ProtocolMessage.Prompt = "admin_consent"; return Task.FromResult(0); }
After you’ve added that code, hit F5 and try signing in. You will be prompted by a dialog similar to the one shown in Figure 8-7.
Figure 8-7 The admin consent dialog.
Superficially, the dialog in Figure 8-7 looks a lot like the one shown in Figure 8-6, but there is a very important difference! The dialog shown when admin consent is triggered has new text, which articulates the implications of granting consent in the admin consent case: “If you agree, this app will have access to the specified resources for all users in your organization. No one else will be prompted.”
Click OK—you’ll end up signing in as usual. The app will look the same, but its entries in the directory underwent a significant change. Once again, take a look at the ServicePrincipal’s oauth2PermissionGrants:
{ "odata.metadata": "https://graph.windows.net/developertenant.onmicrosoft.com/$metadata#oauth2 PermissionGrants", "value": [ { "clientId": "725a2d9a-6707-4127-8131-4f9106d771de", "consentType": "AllPrincipals", "expiryTime": "2016-02-27T00:38:03.4045842", "objectId": "mi1acgdnJ0GBMU-RBtdx3knIMYJNhOpOkFrsIuF86Y8", "principalId": null, "resourceId": "8231c849-844d-4eea-905a-ec22e17ce98f", "scope": "Directory.Write UserProfile.Read", "startTime": "0001-01-01T00:00:00" }, { "clientId": "725a2d9a-6707-4127-8131-4f9106d771de", "consentType": "Principal", "expiryTime": "2016-02-26T18:17:06.8442687", "objectId": "mi1acgdnJ0GBMU-RBtdx3knIMYJNhOpOkFrsIuF86Y_VUmVPfKg_R6aK4EVKgQSW", "principalId": "4f6552d5-a87c-473f-a68a-e0454a810496", "resourceId": "8231c849-844d-4eea-905a-ec22e17ce98f", "scope": "Directory.Write UserProfile.Read", "startTime": "0001-01-01T00:00:00" }, { "clientId": "725a2d9a-6707-4127-8131-4f9106d771de", "consentType": "Principal", "expiryTime": "2016-02-26T00:50:43.3860871", "objectId": "mi1acgdnJ0GBMU-RBtdx3knIMYJNhOpOkFrsIuF86Y9KENMTkWjSRaS-glgajkZb", "principalId": "13d3104a-6891-45d2-a4be-82581a8e465b", "resourceId": "8231c849-844d-4eea-905a-ec22e17ce98f", "scope": "UserProfile.Read", "startTime": "0001-01-01T00:00:00" } ] }
I highlighted the new entry for you: it has a consentType of AllPrincipals, as opposed to the usual Principal. Furthermore, its principalId property does not point to any user in particular; it just says null. This tells Azure AD that the application has been granted a blanket consent for any user coming from the current tenant. To prove that this is really the case, sign out from the app, stop it in Visual Studio, comment out the code you added for triggering admin consent, and start the app again. Sign in as a third user from the same tenant, one that you have never used before with this app. Figure 8-8 shows a visual summary of this oauth2PermissionGrant configuration.
Figure 8-8 An oauth2PermissionGrant recording admin consent enables the app to operate with the requested scope with all users of a tenant at once.
After the credential gathering, you’ll find yourself signed in right away, with no consent prompt of any form.
Application created by an admin user
What happens when you sign in to the Azure portal as an admin user and you create an app in Azure AD? The portal creates the same list of entities: an Application, its ServicePrincipal, and an oauth2PermissionGrant. The difference from the nonadmin case is that the oauth2PermissionGrant for an app created by an admin looks exactly like the one you observed as an outcome of the admin consent flow: it includes consentType allPrincipals, which means that every user in the tenant can instantly get access to the application.
Multitenancy
How to develop apps that can be consumed by multiple organizations is such a large topic that for some time I wondered whether I should devote an entire chapter to it. I ultimately decided against that. Even if this is going to be a very large section, it still is a logical extension of what you have been studying so far in this chapter.
The first part of this section will discuss how Azure AD enables authentication flows across multiple tenants, and how you can generalize what you have learned about configuring the Katana middleware to the case in which users are sourced from multiple organizations.
The second part will go back to the application model proper, showing you what happens to the directory data model when your app triggers consent flows across tenants.
Azure AD as a parametric STS: The common endpoint
Ironically, if you are a veteran of federation protocols, you are at the highest risk of misunderstanding how Azure AD handles multitenancy. The approach taken here is very different from the classic solutions that preceded it, and I have to admit that I myself needed some time to fully grok it.
In traditional claims-based protocols such as SAML and WS-Federation, the problem of enabling access to one application from multiple IdPs has a canonical solution. It entails introducing one intermediary STS (often referred to as resource STS, R-STS or RP-STS) as the authority that the application trusts. In turn, the intermediate STS trusts all the IdPs that the application needs to work with—assuming the full burden of establishing and maintaining trust, implementing whatever protocol quirks each IdP demands. This is a very sensible approach, which isolates the application itself from the complexities of maintaining relationships with multiple authorities. It is also likely the best approach when you don’t know anything about the IdPs you want to connect to, apart from the protocol they implement and the STS metadata they publish. ADFS, Azure Access Control Services (ACS), and pretty much any STS implementation supports this approach.
If you restrict the pool of possible IdPs to only the ones represented by a tenant in Azure AD, however, you have far more information than that, and as you’ll see in the following, this removes the need to have an intermediary in the picture. Although each administrator retains full control over her or his own tenant, all tenants share the same infrastructure—same protocols, same data model, same provisioning pipes. Focusing on endpoints in particular (recall their description from Chapter 3), rather than a collection of STSs for each of its tenants, Azure AD can be thought of like a giant parametric STS, where each tenant is expressed by instantiating its ID in the right segment of the issuance endpoint. Figure 8-9 compares the R-STS approach with the multitenant pattern used by Azure AD.
Figure 8-9 The R-STS brokered trust pattern and the parametric STS pattern. Besides allowing for directory queries that would be impossible via federation alone, the latter makes it possible to automate application provisioning and trust establishment.
In the hands-on chapters, you've experienced directly how the endpoint pattern https://<instance>/<tenant>/<protocol-specific-path> can be modulated to indicate tenant-specific token-issuance endpoints, sign-out endpoints, metadata document endpoints, and so on. You have also seen how the Katana middleware leverages those endpoints for tying one application to one specific tenant. For example, in Chapter 6 you saw how the metadata document published at https://login.microsoftonline.com/DeveloperTenant.onmicrosoft.com/.well-known/openid-configuration (which, by the way, is equivalent to https://login.microsoftonline.com/6c3d51dd-f0e5-4959-b4ea-a80c4e36fe5e/.well-known/openid-configuration, where the GUID is the corresponding tenantID) asserts that tokens issued by that tenant will carry an iss(uer) claim value of https://sts.windows.net/6c3d51dd-f0e5-4959-b4ea-a80c4e36fe5e/. In Chapter 7, you saw how that information is used by the Katana middleware to ensure that only tokens coming from that tenant (that is, carrying that iss value) will be accepted. That’s all well and good, and exactly what you want for line-of-business applications and single-tenant apps in general.
You can repeat the same reasoning for all tenants: all you need to do is instantiate the right domain (or tenantID) in the endpoints paths.
Azure AD makes it possible to deal with multitenant scenarios by exposing a particular endpoint, where the tenant parameter is not instantiated up front. There is a particular value, common, that can be instantiated in endpoints in lieu of a domain or tenantID. By convention, that value tells Azure AD that the requestor is not mandating any particular tenant—any Azure AD tenant will do.
When the endpoint being constructed is one that would serve authentication UI, as is the case for the OAuth2 authorization endpoints, the user is presented with a generic Azure AD credentials-gathering experience. As the user enters his or her credentials, the account he or she chooses will indirectly determine a specific tenant—the one the account belongs to. That will resolve the ambiguity about which tenant should be used for the present transaction, concluding the role of common in the flow. The resulting code or token will look exactly as it would have had it been obtained by specifying the actual tenant instead of common to begin with. In other words, whether you start the authentication flow using https://login.microsoftonline.com/common/oauth2/authorize or https://login.microsoftonline.com/6c3d51dd-f0e5-4959-b4ea-a80c4e36fe5e/oauth2/authorize for an OpenID Connect sign-in flow, if at run time you sign in with a user from the tenant with ID 6c3d51dd-f0e5-4959-b4ea-a80c4e36fe5e, the resulting token will look the same, with no memory of what endpoint path led to its issuance. That should make it even clearer that common is not a real tenant: it’s just an endpoint sleight of hand for late binding a tenant, if you will.
Now comes the fun part. Upon learning about the common endpoint, the typical (and healthy) developer reaction is “Awesome! Let me just change the OpenID Connect middleware options as shown here, and I’ll be all set!”
app.UseOpenIdConnectAuthentication( new OpenIdConnectAuthenticationOptions { ClientId = "c3d5b1ad-ae77-49ac-8a86-dd39a2f91081", Authority = "https://login.microsoftonline.com/common",
Let’s say that you do just that, and then you hit F5 and, just for testing purposes, use the same account you used successfully earlier—the one from the same tenant where the app was defined in the first place. Well, if you do that—surprise! The app won’t work. Sure, upon sign-in you will be presented with your credential-gathering and consent experience, but the app won’t accept the issued token. If you dig in a bit, as you learned in Chapter 7, you’ll discover that the token failed the issuer validation test.
Recall the id_token validation logic from Chapter 7, and the comment about how the discovery document of each tenant establishes what iss value an app should expect. If your app is initialized with a tenant-specific endpoint, it will read from the metadata the tenant-specific issuer value to expect; but if it is initialized with common, what issuer value is it going to get? I’ll save you the hassle of visiting https://login.microsoftonline.com/common/.well-known/openid-configuration yourself: the discovery doc says “issuer”: “https://sts.windows.net/{tenantid}/”. No real tenant will ever issue a token with that value, given that it’s just a placeholder, but the middleware does not know that. That’s the value that the metadata asserts is going to be in the iss claim, and the default logic will refuse anything carrying a different value.
This simply means that the default validation logic cannot work in case of multitenancy. What should you do instead? You already saw the main strategies for dealing with this in Chapter 7, although at the time I could not fully discuss the multitenant case. I recommend that you leaf back a few pages to get all the details, but just to summarize the key points here:
- If you have your own list of tenants that your application should accept, you have two main approaches. If the list is short and fairly static, you can pass it in at initialization time via TokenValidationParameters.ValidIssuers. If the list is long and dynamic, you can provide an implementation for TokenValidationParameters.IssuerValidator where you accommodate for whatever logic is appropriate for your case.
- If the decision about whether the caller should be allowed to get through is not strictly tied to the tenant the caller comes from, you can turn off issuer validation altogether by setting TokenValidationParameters.ValidateIssuer to false. You should be sure that you do add your own validation logic; for example, in the SecurityTokenValidated notifications or even in the app (custom authorization filters, etc.). Otherwise, your app will be completely open to access by anybody with a user in Azure AD. There are scenarios where this might be what you want, but in general, if you are protecting your app with authentication, that means that you have something valuable to gate access to. In turn, that might call for you to verify whether the requestor did pay his monthly subscription or whatever other monetization strategy you are using—and usually that verification boils down to checking the issuer or the user against your own subscription list.
Now that you know how Azure AD multitenancy affects the application’s code, I’ll go back to how consent, provisioning, and the data model are influenced.
Consenting to an app across tenants
The section about the Application object earlier in this chapter, and specifically the explanation of the availableToOtherTenants property, already anticipated most of what you need to know about creating multitenant applications. All apps are created for being used exclusively within their own tenant, and only a tenant admin can promote an app to be available across organizations. Today, this is done by flipping a switch labeled “Application is multi-tenant” on the Configuration page of the application on the Azure portal, and this has the effect of setting the availableToOtherTenants app property to true. Also, an app is required to have an App ID Uri (one of the elements in the identifierUris collection in the Application object) whose host portion corresponds to a domain registered for the tenant. In the sample I have been using through the last couple of chapters, that means that you’d need to set the App ID Uri to something like https://developertenant.onmicrosoft.com/MarioApp1.
Let’s say that you signed in to the Azure portal and modified your app entry to be multitenant. Let’s also say that you modified your app code to correctly handle the validation for tokens coming from multiple organizations. Let’s give the app a spin by hitting F5.
Once the app is running, click the Sign In link, but this time sign in with a user from a different Azure AD tenant. As explained in Chapter 3, in the section “Getting Azure Active Directory,” any Azure subscriber can create a number of Azure AD tenants, create users and apps in them, and so on. If you belong to a big-ish organization, you likely already did this in creating your development tenant, as that’s the best way of experimenting with admin-only features. If you already have a second tenant and an account in it, great! If you don’t, create one tenant, create a user in it, then come back and pick up the flow from here.
Upon successful sign-in, you’ll be presented with the consent page. As you can see in Figure 8-10, the consent page presents some important differences from the single-tenant case. For one, the tenant where the Application object was originally created is prominently displayed as the publisher. Moreover, there’s now text telling you to consider whether you trust the publisher of the application. This is serious stuff—if you give consent to the wrong application for the wrong permissions, the damage to your own organization could be severe. That’s why only admins can publish apps for multiple organizations, and that’s why even the simple Directory.Read permission requires admin consent when it’s requested by a multitenant app.
Figure 8-10 The consent prompt for a multitenant page.
At the beginning of this chapter, you encountered a description of what happens in the tenants for this exact consent scenario: the Application object in the original development tenant is used as a blueprint for creating a ServicePrincipal in the target tenant. In fact, if you query the Applications collection in the target tenant (you’ll learn how to do this in the next chapter), you’ll find no entries with the ClientId of your application—but you will find a ServicePrincipal with that ClientId. From what you have learned a few pages ago, you know that if you look into the collection of oauth2PermissionGrants for that ServicePrincipal, you will find an entry recording the consent of that particular user for this app and the permissions it requires. The principles of admin consent apply here as well: if you want the admin of your prospective customer tenants to be able to grant a blanket consent for all of his or her users, provide a way for your app to trigger an admin consent request.
Changing consent settings
I touched on this earlier, but it’s worth stressing that the list of permissions an app requires isn’t very dynamic. More concretely, say that your application initially declares a certain list of permissions in its requiredResourceAccess, and some users in a few tenants consent to it. Say that after some time you decide to add a new permission. That change in the Application object in your development tenant will not affect the existing oauth2PermissionGrants attached to the ServicePrincipals that have been created at consent time. With this version of Azure AD, the only way of reflecting the new permission set for a given app in a tenant is to revoke the existing consent (typically done by the user visiting myapps.microsoft.com, the Office 365 portal, or any other means that might be available when you read this book) and ask for consent again.
This is less than ideal, especially if you consider that Azure AD offers you no way of warning your users that something changed—you have to handle that in your own app or subscription logic. The good news is that the next version of the Azure AD application model will allow for dynamic consent, solving this issue once and for all.
The last section discussed at length the consent framework used for driving delegated permissions assignment to applications. That is a super important aspect of managing application capabilities, but it is far from the only one. The next section will continue to explore how Azure AD helps you to control how users and applications have access to the directory itself, and to each other.