The Windows Azure AD Application Model
In the various announcements and walkthroughs you had the chance to experience the changes in Windows Azure AD’s product surface. In this (hopefully not too long, it’s midnight already) post I am going to touch on some of the deeper changes that took place beneath the surface. Albeit less evident at first, some of those have far-reaching consequences you should be aware of – especially if you plan to write multitenant applications.
Applications vs. ServicePrincipal
Remember the mega post (oh pioneers!) I wrote when we released the first Web SSO preview, or any of the presentation recordings since then?
One of the key concepts introduced at the time was the idea of ServicePrincipal: an entry in your Windows Azure AD tenant, much like the traditional User Principals, used to describe applications. At the time you just had PowerShell cmdlets to provision applications, and there was no way for you to avoid operating at the ServicePrincipal level.
ServicePrincipals are still the way in which applications are concretely provisioned in a directory. For example, it is the set of AD roles an application’s ServicePrincipal belongs to that determines what the application can do in term of directory access (SSO only, SSO+read access, etc.).
That said: with the GA release, Windows Azure AD introduced a further abstraction level which decouples high level application definition operations from the low-level provisioning of ServicePrincipals.
When you go through an application registration flow in the Windows Azure portal, such as the one described here, you are really doing the moral equivalent of two distinct operations in one.
- You are creating an object of type Application, which describes the main application coordinates (such as URL to use for Web SSO, app id URI to identify the app, client ID and key to be used in OAuth2 flows for invoking the Graph, etc.) and come config settings (such as if the app is single tenant or available for other tenants via consent flows)
- In the same process, the portal is using that Application object as a blueprint for creating a new ServicePrincipal in your tenant. Such ServicePrincipal will have the same coordinates (URLs, URIs, IDs, keys) as the corresponding Application and will also have the access level (SSO, SSO+read, SSO+read+write) you established at creation
The advantage of that will become clear in a moment; before delving into that, let’s stop for a moment and see the above in practice.
Head to your own portal and create an test application. Below you can see what I used for mine:
If I take a look at the entities in the directory after I’ve done that, I’ll find in the applications collection the following new entry:
{
"odata.type": "Microsoft.WindowsAzure.ActiveDirectory.Application",
"objectType": "Application",
"objectId": "b4d66176-4654-4b7d-892e-d3b564bc7910",
"appId": "7042160d-2d58-4528-878f-b05b0edc799e",
"availableToOtherTenants": false,
"displayName": "My test app",
"errorUrl": null,
"homepage": "https://localhost:2121/",
"identifierUris": [
"https://cloudidentity.net/testapp1"
],
"keyCredentials": [],
"logoutUrl": null,
"passwordCredentials": [],
"publicClient": null,
"replyUrls": [
"https://localhost:2121/"
],
"samlMetadataUrl": null
}
Pretty interesting stuff I won’t (yet) go into the details of everything you see there, but for the time being: please note that it closely mirrors the coordinates provided in the portal.
Now, let’s take a peek at the ServicePrincipals collection. Among the many built-in principals you’ll find the following entry:
{
"odata.type": "Microsoft.WindowsAzure.ActiveDirectory.ServicePrincipal",
"objectType": "ServicePrincipal",
"objectId": "a3518a24-2017-4c63-89d3-adec4eeaa9ad",
"accountEnabled": true,
"appId": "7042160d-2d58-4528-878f-b05b0edc799e",
"displayName": "My test app",
"errorUrl": null,
"homepage": "https://localhost:2121/",
"keyCredentials": [],
"logoutUrl": null,
"passwordCredentials": [],
"publisherName": "Vittorio.Bertocci",
"replyUrls": [
"https://localhost:2121/"
],
"samlMetadataUrl": null,
"servicePrincipalNames": [
"https://cloudidentity.net/testapp1",
"7042160d-2d58-4528-878f-b05b0edc799e"
],
"tags": [
"WindowsAzureActiveDirectoryIntegratedApp"
]
}
Yep, that’s a SP with the same coordinates and some extra info. For example, there is a field “publisher” which corresponds to the name of my directory tenant (confusingly named as myself, sorry about that, details here).
There’s more! Let’s take a look at the roles this SP belongs to: it’s easy, you just GET the following: https://graph.windows.net/cloudidentity.net/servicePrincipals/a3518a24-2017-4c63-89d3-adec4eeaa9ad/memberOf where the GUID is the ObjectID. The result:
{
"odata.metadata": "https://graph.windows.net/cloudidentity.net/$metadata#directoryObjects",
"value": [
{
"odata.type": "Microsoft.WindowsAzure.ActiveDirectory.Role",
"objectType": "Role",
"objectId": "88d8e3e3-8f55-4a1e-953a-9b9898b8876b",
"description": "Allows access to various read only tasks in the directory. ",
"displayName": "Directory Readers",
"isSystem": true,
"roleDisabled": false
}
]
}
{
"Status Code" : "BadRequest",
"Description" : "The remote server returned an error: (400) Bad Request.",
"Response" : "{"odata.error":{"code":"Request_BadRequest","message":{"lang":"en",
"value":"Unsupported directory object class 'Application' in query against link 'memberOf'."}}}"
}
The entity type does not even support memberOf. I stand my case.
OK, now let’s make things more interesting. Let’s go back to the portal and make the app available to other tenants. In my case I already picked the URI in the right format (see this) hence all I need to do is flipping the “External access” switch in the config page for the application.
Let’s take a look again at the Application object:
{
"odata.type": "Microsoft.WindowsAzure.ActiveDirectory.Application",
"objectType": "Application",
"objectId": "b4d66176-4654-4b7d-892e-d3b564bc7910",
"appId": "7042160d-2d58-4528-878f-b05b0edc799e",
"availableToOtherTenants": true,
"displayName": "My test app",
"errorUrl": null,
"homepage": "https://localhost:2121/",
"identifierUris": [
"https://cloudidentity.net/testapp1"
],
"keyCredentials": [],
"logoutUrl": null,
"passwordCredentials": [],
"publicClient": null,
"replyUrls": [
"https://localhost:2121/"
],
"samlMetadataUrl": null
}
Yes, AvailableToOtherTenants is now set to true.
The corresponding SP? No changes.
Application, ServicePrincipal and Consent Operations
The introduction of the Application object was largely made for facilitating things in multitenant scenarios. Let’s take a look at that in some details.
I am going to navigate to the app’s consent page and sign in as another tenant’s admin; and I am going to consent to give the app to access the new admin’s directory. At that point, we’ll take a look at what happened in the directory itself.
I’ll start by grabbing the consent URL from the portal and edit it to request a different access level (DirectoryWriters) just to stress my point that it’s independent from the app itself. I’ll ignore the return URL part, we don’t need to write a single line of code for messing with the app settings. The result:
Let’s open another IE in private mode (or another browser type) and paste the consent URL. Then, let’s sign in as the admin of another Windows Azure AD tenant (in my case, treyresearch1.onmicrosoft.com).
Note the access levels.
Click Grant. Of course you’ll be bounced to nowhere, given that the reply URL is bogus, but at that point the consent has been already registered.
Want proof? In the same inPrivate browser navigate to the Windows Azure portal, AD tab, integrated apps: the test app will be there.
As you can see, it’s easy to tell that app apart from the ones developed directly in the tenant. That gets even more evident if you click on its entry:
Note that the access level that “my test app” has in treyresearch1.onmicrosoft.com is different than the one it had in the tenant it was created in, cloudidentity.net.
OK, let’s take a peek under the hood.
First interesting fact: If I query treyresearch1’s applications entities I do not find an entry for “my test app”.
Do you want to take a guess about if I’ll find something in the serviceprincipals? You got it, it’s there!
{
"odata.type": "Microsoft.WindowsAzure.ActiveDirectory.ServicePrincipal",
"objectType": "ServicePrincipal",
"objectId": "6629ad6c-891c-4b2c-97d3-63ec3c6b2579",
"accountEnabled": true,
"appId": "7042160d-2d58-4528-878f-b05b0edc799e",
"displayName": "My test app",
"errorUrl": null,
"homepage": "https://localhost:2121/",
"keyCredentials": [],
"logoutUrl": null,
"passwordCredentials": [],
"publisherName": "Vittorio.Bertocci",
"replyUrls": [
"https://localhost:2121/"
],
"samlMetadataUrl": null,
"servicePrincipalNames": [
"https://cloudidentity.net/testapp1",
"7042160d-2d58-4528-878f-b05b0edc799e"
],
"tags": [
"WindowsAzureActiveDirectoryIntegratedApp"
]
}
Let’s paste again the one we got in cloudidentity.net to spot the differences:
{
"odata.type": "Microsoft.WindowsAzure.ActiveDirectory.ServicePrincipal",
"objectType": "ServicePrincipal",
"objectId": "2be215fa-6970-4ca7-bb1f-2ca304196655",
"accountEnabled": true,
"appId": "7042160d-2d58-4528-878f-b05b0edc799e",
"displayName": "My test app",
"errorUrl": null,
"homepage": "https://localhost:2121/",
"keyCredentials": [],
"logoutUrl": null,
"passwordCredentials": [],
"publisherName": "Vittorio.Bertocci",
"replyUrls": [
"https://localhost:2121/"
],
"samlMetadataUrl": null,
"servicePrincipalNames": [
"https://cloudidentity.net/testapp1",
"7042160d-2d58-4528-878f-b05b0edc799e"
],
"tags": [
"WindowsAzureActiveDirectoryIntegratedApp"
]
}
The only difference is the ObjectId, which of course must be globally unique for every object everywhere. Apart from that, and the different role memberships determined at creation/consent time, the two are identical projections from the original Application’s entry back in the cloudidentity.net tenant.
Things to Note
Here there are few things you want to keep an eye on.
Multitenant Apps Are Tied to Their Origin’s Tenant
The Application object is the enabler of the consent flow magic. Also, the Application object lives in the tenant in which the app was originally created.
That means that the app destiny is tied to the tenant’s, hence you might want to take that into account when taking lifecycle decisions (migrating to new tenants and similar).
Once a ServicePrincipal is Created Via Consent, Ties to the Original Application Object Are Severed
Say that you want to change something in your app, like the app URL. Let’s actually do it and see what happens. Head back to the windows azure portal of the original tenant, and modify the app’s reply URL.
Let’s take a look at the Application object.
{
"odata.type": "Microsoft.WindowsAzure.ActiveDirectory.Application",
"objectType": "Application",
"objectId": "b4d66176-4654-4b7d-892e-d3b564bc7910",
"appId": "7042160d-2d58-4528-878f-b05b0edc799e",
"availableToOtherTenants": true,
"displayName": "My test app",
"errorUrl": null,
"homepage": "https://localhost:2121/",
"identifierUris": [
"https://cloudidentity.net/testapp1"
],
"keyCredentials": [],
"logoutUrl": null,
"passwordCredentials": [],
"publicClient": null,
"replyUrls": [
"https://localhost:2121/tornaacasalasssie"
],
"samlMetadataUrl": null
}
Yep, updated.
What about the ServicePrincipal, still in this tenant?
{
"odata.type": "Microsoft.WindowsAzure.ActiveDirectory.ServicePrincipal",
"objectType": "ServicePrincipal",
"objectId": "83ac2b6e-d20f-4770-874d-4e0859579476",
"accountEnabled": true,
"appId": "7042160d-2d58-4528-878f-b05b0edc799e",
"displayName": "My test app",
"errorUrl": null,
"homepage": "https://localhost:2121/",
"keyCredentials": [],
"logoutUrl": null,
"passwordCredentials": [],
"publisherName": "Vittorio.Bertocci",
"replyUrls": [
"https://localhost:2121/tornaacasalasssie"
],
"samlMetadataUrl": null,
"servicePrincipalNames": [
"https://cloudidentity.net/testapp1",
"7042160d-2d58-4528-878f-b05b0edc799e"
],
"tags": [
"WindowsAzureActiveDirectoryIntegratedApp"
]
}
Updated as well, as expected.
What about the ServicePrincipal provisioned via consent flow in the tenant treyresearch1.onmicrosoft.com?
{
"odata.type": "Microsoft.WindowsAzure.ActiveDirectory.ServicePrincipal",
"objectType": "ServicePrincipal",
"objectId": "6629ad6c-891c-4b2c-97d3-63ec3c6b2579",
"accountEnabled": true,
"appId": "7042160d-2d58-4528-878f-b05b0edc799e",
"displayName": "My test app",
"errorUrl": null,
"homepage": "https://localhost:2121/",
"keyCredentials": [],
"logoutUrl": null,
"passwordCredentials": [],
"publisherName": "Vittorio.Bertocci",
"replyUrls": [
"https://localhost:2121/"
],
"samlMetadataUrl": null,
"servicePrincipalNames": [
"https://cloudidentity.net/testapp1",
"7042160d-2d58-4528-878f-b05b0edc799e"
],
"tags": [
"WindowsAzureActiveDirectoryIntegratedApp"
]
}
Yes, the ServicePrincipal in the other tenant is unaffected by the change in the Application object in the original tenant.
The consent operation uses the state of the Application object at the consent instant and creates a new ServicePrincipal out of it, but after that there is no further synchronization involved. Excluding the programmatic routes, the main way of applying the new settings is to revoke the consent to the app and grant it again.
My interpretation here (remember my disclaimer!!! this is my personal blog!) is that in the power balance between tenant administrator and ISV the directory favors the admin by default. The tenant admin is the king of his own castle, and changes are always under his/her explicit imprimatur: that includes app lifecycle changes such as this one.
This is one of the reasons for which it is super-important for you to avoid exposing unnecessary implementation details when defining the application coordinates. A classic example would be to specify a special page/action in the reply URL instead of referring to the app’s root URL; any changes in that, something that would normally be an implementation detail private to your app, would cause you unnecessary churn.
Bottom line: Before you promote an app to be externally available, it is good practice to ensure that its protocol coordinates are stable; furthermore, it is very important for you to ensure that you have a way of contacting your customers should you need to apply emergency changes. The consent flow is extremely powerful, and allows you to onboard organizational customers with unprecedented ease, but it remains a business critical feature and as such requires thorough planning.
Well, it did take more than one hour to write: I’ll have to crank up the alarm’s volume a bit… but I hope this will come in handy
3 Comments