Application Access Policies in Exchange Online
The Microsoft Graph has been around for a while now and is slowly turning into the de-facto standard API for any Office 365 developer, including those focused on Exchange Online. Microsoft has already announced plans to stop any feature development for the EWS API and focus on the Graph instead. While EWS will continue working for the foreseeable future, going forward you will have to switch to the Graph, so understanding how to control it is vital.
A not so short introduction to the Graph API
Switching to the Graph comes with somewhat of a learning curve. It’s beyond the scope of the article to give you a complete introduction, but in order to better understand the examples shown here, you will be required to have at least a basic understanding of the concepts behind OAuth, OIDC, application registration, permission roles and scopes, consent, access tokens and so on. Microsoft has published extensive documentation on all these topics and this article is a good starting point.
To give you the TL;DR version – the Graph allows you to perform various actions against different resources in Office 365, such as creating a message, deleting a file, or accessing a report. You can run those in the context of a given user, or as a background service. The authentication process is performed against the Microsoft Identity platform by means of obtaining tokens, which are then presented to the workload against which you want to perform a given operation. For authentication to happen, the app must be recognized by the Microsoft Identity platform, in other words an application registration must be performed by the developer first. To control which actions a given application can perform, the developer describes the permissions needed by the app and you, or an admin in the tenant, has to grant (“consent” to) the permissions.
Here lies one of the problems when using the Graph. As it tries to cover quite diverse set of resources, the permissions model used by the Graph doesn’t go into the peculiarities of each individual workload. Instead, the permission scopes introduced only cover the basic scenarios: access only your own resources, access resources shared with you, access all resources. For applications running in the scope of a given user, the so-called delegated permissions model, this is rarely a problem, as the workload can trim those permissions to cover just the resources the user has access to. For the so-called application permissions however, where the app runs without a signed in user, access is given to any and all resources.
To give a specific example, the Files.ReadWrite.All delegated permission will allow an application to perform all operations against all files a given user has access to. The permissions given to the user across SharePoint Online and OneDrive for Business site collections will be honored, so the user cannot just go a bulk-delete all files in another person’s OneDrive, unless he has been explicitly granted access to it. On the other hand, the Files.ReadWrite.All application permission will allow an application to perform all operations against all files in the tenant, period. While these permissions are only needed for a small set of operations and can only be granted by an administrator, every app out there can simply request them, and the fact that by default end users are allowed to add such apps to the directory doesn’t help either. In effect, it takes a single misplaced click to grant an app full access to all resources in your organization, and we have already seen bad actors exploiting this in Office 365.
Restricting Graph API calls via Application Access Policies
After this introduction, let’s talk about a cool new feature that allows us to mitigate threats and scope down the permissions given to only a subset of the mailboxes in our tenant. The feature is called Application Access Policies and, in a nutshell, represents a list of mailboxes a given application is allowed to run calls against. It is important to understand that this is a workload-specific feature, not a Graph one, the workload in question of course being Exchange Online. When Exchange Online receives a request to execute an operation from any of the Graph endpoints it exposes, the appID included as part of the access token, which in turn is included in the request headers, is checked against the Application Access Policies list and only allowed to execute against mailboxes included in the policy scope.
Let’s dig into some specifics. Consider an app registration in Azure AD for NewApp, to which I rather generously added the entire set of permissions available for Exchange Online, as illustrated on the screenshot below. Those include things like being able to access all items in all mailboxes in the tenant, change settings for any mailbox, being able to send messages as any user, etc. And since this is an web app type of application, it runs without a user context, and anyone that can run operations via this application will have practically unrestricted access to all mailboxes in the company, regardless of any roles he or she holds inside Office 365.
As an example of the things one can do with access to said application, here’s a PowerShell code snippet that gets a token for the NewApp application, then uses it to get a list of Inbox rules for one of the mailboxes in the company. The same example can be used against any mailbox thanks to the permissions granted on the app.
So how do we go about restricting this? As mentioned above, Applications Access Policies enforce a list of objects against which we can use a given application. For this specific example, we can configure an Application Access Policy that limits the NewApp application to just a subset of the mailboxes in the tenant, or similarly, prevents it from running against the most sensitive ones. Apart from the application identifier, we need to specify a group object, members of which will populate the restricted access list. Once we have all the needed information, we can run the New-ApplicationAccessPolicy cmdlet:
The moment the first policy is created, it becomes active almost immediately, as the restrictions are enforced directly at the Exchange layer. If we now try to run the same query against the Graph API, but for one of the mailboxes under the scope of the newly created policy, we would get the following:
The (403) Forbidden error can be generated due to variety of reasons, so if we want to be absolutely certain that the restrictions of the newly created policy apply, we will have to get the full server response. This is not as straightforward to do in PowerShell, but the following code snippet should help:
And there we have it – the “Access to OData is disabled.” error is thrown, as detailed in the documentation. In effect, the “NewApp” application is now restricted and cannot act against any of the mailboxes that are members of the “USG” group we specified when creating the policy. This is because we used the DenyAccess value for the –AccessRights parameter, which means that every request for accessing any of the members of the group will be denied. Alternatively, one can use RestrictAccess, which is an inclusion type of policy – the API calls will only be allowed to run against mailboxes that are members of the group.
Additional information about Application Access Policies
Here’s probably the correct place to detail the rules of applying Application Restriction Policies:
- The DenyAccess action has priority over the RestrictAccess action.
- If a DenyAccess policy exists for given Application and Target Mailbox pair, the app’s access request is denied.
- If a RestrictAccess policy exists for given Application and Target Mailbox pair, the app’s access request is granted.
- If a RestrictAccess policies exists for given Application, but does not match a Target Mailbox, the app’s access request is denied.
- If none of the above conditions are met, then the application is granted access to the requested target mailbox.
So, in a nutshell, for any given application you can create a policy with either a DenyAccess (exclusive filtering) action, or RestrictAccess (inclusive filtering), with the former having precedence. If a mailbox does not match any restrict policy, no restrictions are applied and thus the default level of access is not changed unless you create a policy. You can also create a policy with asterisk (“*”) used as the AppId – such catch-all policy will apply to all applications trying to run API calls against Exchange Online and is a great feature to have.
Let’s also look at the other cmdlets used for working with Application Restriction Policies. The Get-ApplicationAccessPolicy cmdlet can be used to list the policies configured in the tenant. As shown below, the Identity of each policy is constituted of the tenant identifier followed by the “\” char, the application id, a colon (“:”), the SID of the group, a semicolon (“;”) and the objectID of the group.
Although a Set-ApplicationAccessPolicy cmdlet exists, it can only be used to change the Description of a policy, so it’s mostly useless. Remove-ApplicationAccessPolicy can be used to delete any existing policies, and the Test-ApplicationAccessPolicy can be used to make sure the resultant set of policies configured for a given app will produce the desired outcome when applied to a given mailbox or group of mailboxes.
Some issues and limitations
Lastly, let’s also mention few issues you might run into when playing with application access policies. Despite what the documentation claims, you cannot use a regular DG or a dynamic DG for the –PolicyScopeGroupId parameter, only mail-enabled security groups are accepted. It’s also unfortunate that you cannot disable a given policy, you have to remove it instead. Speaking of removal, the action will not prompt for confirmation, so make sure you know what you are doing.
You might also experience some delays in applying the policies (about an hour), due to the caching mechanisms employed by Exchange Online for both the group membership and the policy objects. As the restrictions are applied at the Exchange server layer, obtaining a new token will not help speed things up, you have to wait for the policy changes to propagate. The Test–ApplicationAccessPolicy cmdlet will help you validate without having to wait.
It’s also important to understand that not all API calls are currently covered, just the ones included in the following scopes:
Probably the most important thing is that we cannot apply Application Access Policies against EWS impersonation granted as application permissions. This is unfortunate, as impersonation is one of the most powerful permissions you can have, especially when granted via application permissions (if granted via delegate permissions, we can control impersonation via RBAC scopes).
Overall though, this functionality is a great addition to the service, and something that should have been included from the get go. Kudos to the Exchange team for providing these controls!