9

I have set up two App Services in Azure. 'Parent' and 'Child', both expose API endpoints.

  • Child has endpoint 'Get'.
  • Parent has endpoints 'Get' and 'GetChild' (which calls 'Get' on Child using HttpClient).

I want all Child endpoints to require auth via Managed Identity and AAD, and I want all Parent endpoints to allow anonymous. However in Azure I want to set the Parent App Service to have permission to call the Child App Service. Therefore Child endpoints are only accessible by using Parent endpoints (or if you have permissions on a user account to directly use Child).

In the Azure Portal:

Authentication/Authorization

  • I have enabled 'App Service Authentication' on both App Services.
  • Child is set to 'Log in with AAD'.
  • Parent is set to 'Allow Anonymous requests'.
  • Both have AAD configured under 'Authentication Providers'.

Identity

  • Set to 'On' for both App Services

Access control (IAM)

  • Child has Parent as Role Assignment, Type = "App Service or Function App" and Role = "Contributer"

With all the above setup:

  • Calling Child -> Get, requires me to log in
  • Calling Parent -> Get, returns the expected response of 200 OK
  • Calling Parent -> GetChild, returns "401 - You do not have permission to view this directory or page"

Without the use of Client ids/Secrets/Keys/etc, as I thought the idea behind Managed Identity was to throw that all out the window, given all the above, should Parent be able to call Child? And if so, what have I setup wrong?

user10238915
  • 93
  • 1
  • 3

2 Answers2

8
  • Calling Parent -> GetChild, returns "401 - You do not have permission to view this directory or page"

Without the use of Client ids/Secrets/Keys/etc, as I thought the idea behind Managed Identity was to throw that all out the window, given all the above, should Parent be able to call Child? And if so, what have I setup wrong?

There are two things that I notice with current setup.

1. Acquire a token using Managed Identity to call "Child" service endpoint from "Parent"

Managed Identity only provides your app service with an identity (without the hassle of governing/maintaining application secrets or keys). This identiy can then be used to acquire tokens for different Azure Resources.

But it is still your App's responsibility to make use of this identity and acquire a token for relevant resource. In this case the relevant resource will be your "Child" API. I think this is probably the part you are missing right now.

Relevant documentation on Microsoft Docs - How to use managed identities for App Service and Azure Functions > Obtain tokens for Azure resources

using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Azure.KeyVault;
// ...
var azureServiceTokenProvider = new AzureServiceTokenProvider();
string accessToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://vault.azure.net");

// change this to use identifierUri for your child app service. 
// I have used the default value but in case you've used a different value, find it by going to Azure AD applications > your app registration > manifest
string accessToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://<yourchildappservice>.azurewebsites.net");

This C#/.NET sample uses Microsoft.Azure.Services.AppAuthentication nuget package and acquires a token for Azure Key Vault. In your case, you will replace https://vault.azure.net with the identifierUri for your "Child" service. It's usually set to https://<yourappservicename>.azurewebsites.net by default, but you can find it's value by going to Azure AD applications and then finding the relevant app registration > manifest. You could also use applicationId for the target application (i.e. "Child") to acquire the token.

In case you're not using C#/.NET, same Microsoft Docs link above also has guidance on how to acuqire token using Managed Identity and REST based calls from any platform. Using REST Protocol

Here is a blog post that also gives a good walk through - Call Azure AD protected website using Managed Service Identity (MSI)

2. Azure RBAC Role Assignments are different from Azure AD roles that you may want to use

I see that you have assigned contributor role to Parent App Service's identity from IAM. This role assignment works for Azure RBAC and help in giving permissions for managing the resources, but Azure AD role claims work differently.

If what you were looking to do is to assign a role to parent app, which can be checked in child app and only then allow the calls there is a different way of setting this up.

I should first mention that this role based setup is for a little advanced scenario and not really mandatory to do. You should be able to call "Child" service from "Parent" once you follow the steps in point 1 described above.

Now once the call from Parent to Child is working, you may want to limit the access to Child app service to only "Parent" or a few valid applications. Here are two approaches to achieve that.

Both the approaches are explained on Microsoft Docs here - Microsoft identity platform and the OAuth 2.0 client credentials flow

Relate SO Posts and Blog

Approach 1 - Use Access Control Lists

When your "Child" API receives a token, it can decode the token and extract the client's application ID from the appid and iss claims. Then it compares the application against an access control list (ACL) that it maintains.

Depending on your requirement, API might grant only a subset of full permissions or all permissions to a specific client.

Approach 2 - Use Application Permissions or Roles

Configure your child API application to expose a set of application permissions (or roles).

This approach is a little more declarative, as you define an application permission that needs to be assigned to any application that can call your child-api.

Navigate to Azure Active Directory > App Registrations > App registration for your child-api app > Manifest

Add a new application role.. using json like this:

"appRoles": [
{
  "allowedMemberTypes": [
    "Application"
  ],
  "displayName": "Can invoke my API",
  "id": "fc803414-3c61-4ebc-a5e5-cd1675c14bbb",
  "isEnabled": true,
  "description": "Apps that have this role have the ability to invoke my child API",
  "value": "MyAPIValidClient"
}]

Assign the app permission to your frontend app

New-AzureADServiceAppRoleAssignment -ObjectId <parentApp.ObjectId> -PrincipalId <parentApp.ObjectId> -Id "fc803414-3c61-4ebc-a5e5-cd1675c14bbb" -ResourceId <childApp.ObjectId>

Now, in the auth token received by your child api, you can check that the role claims collection must contain a role named "MyAPIValidClient" otherwise you can reject the call with Unauthorized exception.

BillH
  • 197
  • 1
  • 9
Rohit Saigal
  • 9,317
  • 2
  • 20
  • 32
  • Thank you Rohit for such a comprehensive answer, I have implemented your code snippet (I am using C#.NET), and I am receiving a token back. I add that token as a Bearer token to the HttpClient request to Child -> Get, but I still receive a 401. I recreated my app services from scratch to ensure I hadn't set any settings unintentionally. Have you any more you can give? – user10238915 Jun 12 '19 at 07:53
  • I have actually managed to get this to work, by using the ApplicationId (ClientId), in place of the identifierUri. Which whilst great as I have a working end-to-end, I would prefer something more human readable, if you do have any suggestions? – user10238915 Jun 12 '19 at 08:20
  • @user10238915 you're welcome. About acquiring token, you should ideally be able to do it either of them i.e. applicationId or identifierUri.. I'm not sure why only applicationId is working for you. One thing I can suggest is make sure the identifierUri is present in allowed token audiences.. see step 4 of the link I already shared in my answer as well.. https://blogs.msdn.microsoft.com/waws/2018/10/23/call-azure-ad-protected-website-using-managed-service-identity-msi/ – Rohit Saigal Jun 13 '19 at 04:37
  • The anchor in the 1st link is wrong - it should start "#obtain-" not "#obtaining-". (I suggested an edit, but it was rejected). – BillH Sep 16 '20 at 17:22
  • makes sense @BillH, not sure why the edit was rejected, but looks like you're correct and link on MS Docs has changed since I originally posted the answer. Thanks for the information, I'll update it. – Rohit Saigal Sep 16 '20 at 17:26
  • See the "APIs and other Azure AD registered applications" section in this article which expands on Approach 2 discussed above https://blog.yannickreekmans.be/secretless-applications-add-permissions-to-a-managed-identity/ – Dasith Wijes Nov 17 '21 at 05:32
  • @user10238915 you can use the following snippet here which use the now recommended "Azure.Identity" library for Managed Identity https://gist.github.com/dasiths/80a5a8b56c1bb33dcb940d8a3ae39f37 var token = await azureAdTokenRetriever.GetTokenAsync("resource app id or uri", "required scopes"); var autheHeader = new AuthenticationHeaderValue("Bearer", token); – Dasith Wijes Nov 17 '21 at 05:40
1

To expand on the accepted answer.

  1. You need to define an "App Role" in the target app registration's manifest. This is the app registration which is used to represent the resource (API App Service).

  2. Then you use the Azure CLI to grant permission for that "App Role" to the Enterprise App (The one generated when you setup a managed identity for the client app). See the "APIs and other Azure AD registered applications" in this article for detailed steps https://blog.yannickreekmans.be/secretless-applications-add-permissions-to-a-managed-identity/

You can retrieve the token using the following once the permissions have been granted. The code snippet below uses Azure.Identity which is now the recommended library for Managed Identity in Azure.

public class AzureAdTokenRetriever : IAzureAdTokenRetriever
{
    private readonly ILogger<AzureAdTokenRetriever> logger;
    private readonly IMemoryCache inMemoryCache;

    public AzureAdTokenRetriever(
        ILogger<AzureAdTokenRetriever> logger,
        IMemoryCache inMemoryCache)
    {
        this.logger = logger;
        this.inMemoryCache = inMemoryCache;
    }

    public async Task<string> GetTokenAsync(string resourceId, string scope = "/.default")
    {
        var resourceIdentifier = resourceId + scope;
        if (inMemoryCache.TryGetValue(resourceIdentifier, out var token))
        {
            this.logger.LogDebug("Token for {ResourceId} and {Scope} were fetched from cache", resourceId, scope);
            return (string)token;
        }

        var tokenCredential = new DefaultAzureCredential();
        var accessToken = await tokenCredential.GetTokenAsync(
            new TokenRequestContext(new [] { resourceIdentifier }), CancellationToken.None)
            .ConfigureAwait(false);

        // Set cache options with expiration 5 minutes before the token expires
        var cacheEntryOptions = new MemoryCacheEntryOptions().SetAbsoluteExpiration(accessToken.ExpiresOn.AddMinutes(-5));
        inMemoryCache.Set(resourceIdentifier, accessToken.Token, cacheEntryOptions);
        this.logger.LogDebug("Token for {ResourceId} and {Scope} saved in cache with expiration of {TokenExpiry}",
            resourceId, scope, cacheEntryOptions.AbsoluteExpiration);

        return accessToken.Token;
    }
}
Dasith Wijes
  • 1,328
  • 12
  • 22