Thursday, May 9, 2024

AUTHENTICATING TO AZURE AD PROTECTED APIS WITH MANAGED IDENTITY — NO KEY VAULT REQUIRED

 


Authenticating to Azure AD protected APIs with Managed Identity — No Key Vault required

A common way of authenticating to APIs, such as Microsoft Graph, has been that you set up an application registration in Azure AD, and create a client secret or a certificate. Then you store that sensitive information in an Azure Key Vault and have your application fetch it from there using its managed identity. After that, you’d use that fetched piece of information for authenticating to the API in your code. But did you know that there’s also an alternative way, which allows us to skip creating the application registration, client secret/certificate, and Azure Key Vault entirely?

We can allow our applications to authenticate to Azure AD protected APIs directly with the application managed identity. We don’t need to have a client secret or a certificate, and we can get rid of the risk of those pieces of sensitive information getting leaked. Otherwise, it works seemingly the same way by allowing us to use APIs with application permissions; it is just more secure.

Managed identity is not a new feature, and I know a few people have already blogged about this topic in the past. Still, I continue to run into developers who are not familiar with this approach. Based on my experience, it is clear that this method requires more exposure, which is why I am here writing about it for you. I hope you’ll find this article useful, and love using managed identity for authenticating to Azure AD protected APIs just as much as I do.

The Managed Service Identity (MSI) feature has been renamed to Managed identities for Azure resources. In this blog post, I’ll mostly be referring to this feature simply as managed identity.

WHAT ARE MANAGED IDENTITIES FOR AZURE RESOURCES?

A managed identity allows an Azure-hosted app to access other Azure AD protected services without having to specify explicit credentials for authentication. When you enable the managed identity for your app, a service principal gets created for your application in Azure AD. You can then grant that principal varying levels of permissions to the Azure resources and APIs used by your application. When we do not need to use and store separate credentials, it automatically improves the security of our application.

“When you enable managed identity on your web app, Azure activates a separate token-granting REST service specifically for use by your app. Your app will request tokens from this service instead of directly from Azure Active Directory. Your app needs to use a secret to access this service, but that secret is injected into your app’s environment variables by App Service when it starts up. You don’t need to manage or store this secret value anywhere, and nothing outside of your app can access this secret or the managed identity token service endpoint.” [source]

Managed identity is available for many Azure resources, such as Azure Functions, Web Apps and Logic Apps. You can check the Microsoft documentation for a full and up-to-date list of all the resource types that support managed identities.

SYSTEM ASSIGNED VS. USER ASSIGNED MANAGED IDENTITIES

We have two different types of managed identities: system assigned and user-assigned. Both of them work pretty much the same way from the authentication perspective, but they are administered quite differently. Being able to make the correct decision between them based on your scenario can help you reduce administrative overhead.

A system-assigned managed identity is always tied to just that one resource where it is enabled. You then control the permissions for that application individually. This is the preferred approach if your apps need different roles for different services.

A user assigned managed identity is created as a separate Azure resource. It can then be given permissions to services and added to multiple different Azure resources. This is the preferred approach if several apps need the same permissions to the same services.

Note that you don’t necessarily need to choose to use just either a system assigned or a user-assigned managed identity. Depending on the Azure resource, you can often combine both and use multiple user-assigned managed identities. You should always follow the principle of least privilege, and plan the permissions of the identities to be as fine-grained as possible.

ENABLING THE MANAGED IDENTITY FOR YOUR AZURE APPLICATION

You can enable the system managed service identity for an Azure resource by going to its Identity blade, changing the Status to On and then hitting Save. After saving, you’ll see a GUID in the Object ID field. This is the ID of the managed service identity. You’ll need the ID for managing your app’s permissions.

If you wish to use the user-assigned managed identities instead, you first need to create the User Assigned Managed Identity Azure resource, and then add it on the User assigned tab. You can see the object ID of the user assigned managed identity on its Overview blade.

Managed identity enabled

MANAGING YOUR APP’S PERMISSIONS TO THE API

A widespread approach has been to enable the managed identity so that your app can securely access sensitive information stored in an Azure Key Vault. We’d do this for, e.g., getting a client secret from the key vault for authenticating to Microsoft Graph. However, it is also possible for us to grant our application permissions directly to Microsoft Graph (and other APIs) with PowerShell, which essentially allows us to skip the whole Azure Key Vault step entirely. At the time of this writing, PowerShell is our only option for performing this task; there is no way to do it in the Azure Portal.

For managing your application’s permissions using PowerShell, you first need to install the Azure AD PowerShell module, if you have not yet done so. You can install the module by running Windows PowerShell as an administrator, and then executing command Install-Module AzureAD.

Before you run the scripts, replace the miObjectId variable value with the ID of your application managed identity. You can see the ID in the view where you enabled the feature.

Also, if you want to manage your application’s permissions to some other API than Microsoft Graph, replace the appId variable value with the ID of the other API. You can find the ID of the API in the Enterprise applications view in Azure AD. There, select All Applications as the Application type, and search for your API. Then copy the GUID from the Application ID column.

Enterprise applications view

ADDING PERMISSIONS

You can use the following script for granting permissions to an Azure AD protected API for your Azure-hosted app that has managed identity enabled. Replace the permissionsToAdd variable value with the API permissions/roles your application requires.

The user executing the script needs to have either Application administrator, Cloud application administrator or Global administrator role.

# Replace with your managed identity object ID
$miObjectID = "17707c90-dab4-483d-a57f-65e91ac3d94f"
# The app ID of the API where you want to assign the permissions
$appId = "00000003-0000-0000-c000-000000000000"
# The app IDs of the Microsoft APIs are the same in all tenants:
# Microsoft Graph: 00000003-0000-0000-c000-000000000000
# SharePoint Online: 00000003-0000-0ff1-ce00-000000000000
# Replace with the API permissions required by your app
$permissionsToAdd = "User.Read.All", "User.Invite.All", "GroupMember.ReadWrite.All"
Connect-AzureAD
$app = Get-AzureADServicePrincipal -Filter "AppId eq '$appId'"
foreach ($permission in $permissionsToAdd)
{
try {
$role = $app.AppRoles | where Value -Like $permission | Select-Object -First 1
New-AzureADServiceAppRoleAssignment -Id $role.Id -ObjectId $miObjectID -PrincipalId $miObjectID -ResourceId $app.ObjectId -ErrorAction Stop
}
catch {
if ($_.Exception.ErrorContent.Message.Value -notcontains "Permission being assigned already exists on the object") {
throw $_
}
}
}

CHECKING EXISTING PERMISSIONS

If you later need to check what permissions your app has to the API in question, you can do that with the following script. You can run this script without the administrator roles mentioned above.

# Replace with your managed identity object ID
$miObjectID = "17707c90-dab4-483d-a57f-65e91ac3d94f"
# The app ID of the API where you want to assign the permissions
$appId = "00000003-0000-0000-c000-000000000000"
# The app IDs of the Microsoft APIs are the same in all tenants:
# Microsoft Graph: 00000003-0000-0000-c000-000000000000
# SharePoint Online: 00000003-0000-0ff1-ce00-000000000000
Connect-AzureAD
$app = Get-AzureADServicePrincipal -Filter "AppId eq '$appId'"
$appRoles = Get-AzureADServiceAppRoleAssignment -ObjectId $app.ObjectId | where PrincipalId -eq $miObjectID
foreach ($appRole in $appRoles) {
$role = $app.AppRoles | where Id -eq $appRole.Id | Select-Object -First 1
write-host $role.Value
}

REMOVING PERMISSIONS

If your app no longer needs some of the permissions granted earlier, you can remove them individually with the following script.

# Replace with your managed identity object ID
$miObjectID = "17707c90-dab4-483d-a57f-65e91ac3d94f"
# The app ID of the API where you want to assign the permissions
$appId = "00000003-0000-0000-c000-000000000000"
# The app IDs of the Microsoft APIs are the same in all tenants:
# Microsoft Graph: 00000003-0000-0000-c000-000000000000
# SharePoint Online: 00000003-0000-0ff1-ce00-000000000000
# Replace with the permissions to remove
$permissionsToRemove = "Group.ReadWrite.All", "Sites.ReadWrite.All"
Connect-AzureAD
$app = Get-AzureADServicePrincipal -Filter "AppId eq '$appId'"
$appRoles = Get-AzureADServiceAppRoleAssignment -ObjectId $app.ObjectId | where PrincipalId -eq $miObjectID
foreach ($appRole in $appRoles) {
$role = $app.AppRoles | where Id -eq $appRole.Id | Select-Object -First 1
if ($permissionsToRemove.Contains($role.Value)) {
Remove-AzureADServiceAppRoleAssignment -ObjectId $app.ObjectId -AppRoleAssignmentId $appRole.ObjectId
}
}

AUTHENTICATING USING THE MANAGED SERVICE IDENTITY

Here I have a couple of examples for you of how to use a managed identity for authentication in your solution. First, we have a .NET Core solution, and then I’ll show you how we can easily authenticate in an Azure Logic App using its managed identity. Again, I’m using Microsoft Graph as the API in both of these examples.

.NET CORE

As the first thing, install the Microsoft.Azure.Services.AppAuthentication NuGet package to your project, and specify a matching using statement for your namespace: using Microsoft.Azure.Services.AppAuthentication;

The method below will get an access token for the specified API using the managed identity of the Azure resource where your app is running. The resourceUrl (the URL address of the API) is the only mandatory parameter. The second parameter appId should only be specified when you are using the user-assigned managed identity. When the second parameter is not provided, system-assigned managed identity will be used by default.

/// <summary>
/// Gets an access token using the managed identity of the Azure resource.
/// </summary>
/// <param name="resourceUrl">The URL of the API, e.g., https://graph.microsoft.com</param>
/// <param name="appId">Optional user assigned managed identity client ID. Defaults to system assigned identity.</param>
/// <returns>Returns the Bearer access token value</returns>
public async Task<string> GetAccessTokenAsync(string resourceUrl, string appId = null)
{
string connectionString = appId != null ? $"RunAs=App;AppId={appId}" : null;
var tokenProvider = new AzureServiceTokenProvider(connectionString);
return await tokenProvider.GetAccessTokenAsync(resourceUrl);
}

Note that the code snippet above does not work while you are debugging your code locally. After all, your app needs to have an Azure managed identity for authentication, which your app will only have when it is hosted in Azure. It is up to you to decide how you wish to implement authentication for local debugging. Alternatively, you can deploy your app to Azure and debug it remotely.

AZURE LOGIC APPS

Calling an API in Azure Logic Apps with the HTTP action by using a managed identity is super easy. You first add either a system-assigned or a user-assigned managed identity for your app — just like you would for any other Azure resource. Then, while configuring the HTTP action, you select Managed Identity as the authentication type and select the identity from the dropdown. The only thing that requires a bit of typing regarding authentication is the Audience which is the URL of the API you want to call.

Note that Azure Logic Apps can only have either a system-assigned or user-assigned managed identity; it can’t have both. Moreover, when using user-assigned managed identities, you can only add one identity to your app.

AFTERWORD

So, what do you think of using managed identities for authentication? They are pretty awesome, eh? If you have any thoughts, comments or questions about the topic, feel free to write about them to me using the comments section below. I genuinely hope you enjoyed reading this article and found it useful.

Also, if you’d like to consume more content from me, feel free to subscribe to my Insider (news)letter and follow me on your favourite social media platforms (TwitterYouTubeLinkedInGithub). I share different types of content on each one of them.

Thank you for reading, it is always appreciated, and until next time!

BFF Back-End for Front-End Architecture

AWS AppSync and Aurora Serverless let you easily build a GraphQL powered BFF to decouple your Front-End Clients from the complex “Deep Back-End”

What?

A BFF or Back-End for Front-End Architecture or Pattern basically means that you have a dedicated Back-End for the soul purpose of serving your Front-End in the most efficient and focused way.

Why?

Especially in enterprise application you notice that the traditional Back-End is moving extremely slow. Usually because it has grown to a very fragile complexity. Connecting the Front-End to it is often done with very monolithic endpoints which are equally fragile and complex.

But times have changed and product management expects much more from Front-Ends now. They need to be modern, agile, fast iterations following customer feedback and user research. And with modern Front-End technologies like React or Flutter it is easy to achieve this.

The roadblock is the traditional Back-End. It usually takes a long time to evolve and often doesn’t match exactly what the Front-End actually demands. Which ends up in slow over-fetching and lots of unnecessary untangling of data on the Client. Which all leads to extremely slow, messy and painful Front-End development even with the most modern Front-End technologies.

So?

BFF to the rescue. Imagine having a Back-End that is dedicated and owned by the Front-End. The Front-End can evolve and dictate exactly what it needs in which shape and form from the Back-End at any time.

To achieve this the Back-End must be owned by the Front-End (developers). Whenever the Front-End changes and needs a changed response from the Back-End the Front-End developers can just change the Back-End without having to ask for permission or wait for anybody else.

How?

Here is my very opinionated way of achieving all of this.

GraphQL is the perfect query language to interface between Front-End and Back-End. It allows the Front-End to specify the exact structure of the data and interaction it expects from the Back-End if form of a flexible Schema.

Cognito is a serverless service that provides us with very flexible User Authentication and Management.

AppSync is a serverless service that provides us with highly scalable GraphQL Endpoints.

Aurora is a serverless SQL database that provides us with highly scalable Database Clusters.

Pulumi is a framework that lets you create your infrastructure as code. This allows us to completely* automate and reproduce our BFF infrastructure across multiple teams, features, products and stages with continues deployment and DevOps procedures in mind.

“Wait, that’s all on AWS” I hear you saying! And right you are, as I said this is very opinionated and I believe it is currently the only way to achieve this completely serverless and automated at an enterprise scale.

Details?

Let’s start of with creating our desired infrastructure as code.

If you haven’t already, please create a free AWS account as well as follow the Pulumi Getting Started Instructions and Pulumi AWS Setup Instructions.

Now create a new empty folder for your project, cd into it and create a new Pulumi project.

mkdir mybff
cd mybff
pulumi new aws-javascript

Configure your project to use the right AWS region and profile by editing the mybff.dev.yaml file.

config:
aws:region: us-east-1
aws:profile: mybff-aws-profile

Awesome, now we are ready to go and add our first bit of infrastructure to the index.js file.

Let’s start with some boilerplate which imports the necessary Pulumi dependencies.

Now we can add the Cognito infrastructure. This will give you a Cognito User and Identity Pool with the ability to sign in with Federated Identity Providers like Google or Facebook.

Let’s create the Aurora Serverless Database Cluster.

To create the AppSync GraphQL API let’s first add some functionality to the top of the index.js.

Notice that this will load the GraphQL Schema from a file called schema.graphql. So we have to create it.

We can add the first part of our AppSync resources now.

And now we actually have to run

pulumi up

Because unfortunately at the moment Terraform and therefore Pulumi do not know how to create AppSync DataSources that connect to Aurora Serverless. So we have to do this step manually once at this point.

  1. Go to your AWS RDS console for your Aurora Database
  2. Select your newly created Database Cluster and press Modify
  3. Under Network & Security you have to check the Data API checkbox
  4. Press Continue
  5. Choose Apply Immediately and press Modify Cluster

Now that we have enabled the Data API for your Database Cluster we can create an AppSync DataSource for it.

  1. Go to your AWS AppSync console for your GraphQL API
  2. Click your newly created GraphQL API to edit it
  3. Click on Data Sources and then on Create data source
  4. For Data source name enter graphQLDataSource_manual
  5. For Data source type select Relational database
  6. For Region choose us-east-1
  7. Select your newly created Database Cluster from the dropdown
  8. Select your newly created Secret from the dropdown
  9. Choose Existing role and select your newly created “graphQLDataSourceServiceRole” role from the dropdown

Now we can add the last bit that dynamically creates the Resolvers for the Schema.

This will create resolvers for the files it finds in the graphql/resolvers/ folder. This assumes that the filename for each resolver is Type.property.js. For example, to add a resolver for the listPets property on the Query type we create a file called Query.listPets.js in the graphql/resolvers/ folder.

Now we can run

pulumi up

again and this will create the resolver resource for us.

From now on everytime you want to create/update/delete an AppSync GraphQL Resolver you just have to create/update/delete the resolver files in the resolvers folder and run “pulumi up” again ðŸŽ‰

Awesome, now what do we have achieved so far?

  • A Cognito User and Identity Pool that manages your users and allows us to get credentials to access the AppSync GraphQL API.
  • An Aurora Serverless Database Cluster that gives us a SQL database which only costs us money when there are actually users making requests and automatically scales to “infinity”.
  • A SigV4 protected, flexible GraphQL API which lets us evolve our schema and add new resolvers fully managed as part of our infrastructure code.

Why is this awesome?

  • Our BFF is completely* automated using infrastructure as code.
  • It can be deployed even into the most complex enterprise environments with the highest compliance restrictions thanks to the inbuilt compliance and scalability of AWS services.
  • Being infrastructure as code gives DevOps total control and visibility when it comes to versioning, deployment and integration using Git branches and PRs.
  • The BFF finally empowers the Front-End and UX to evolve fast without dependencies and clear boundaries.
  • Clear boundaries also provide clean and clear integration points between the BFF and the “deep” Back-End allowing improved architecture and security.
  • In an enterprise environment it allows your teams to own their features/products, to use Git branches for different stages and automatic continues deployment to development and test environments identically to the production environments.
  • It improves the overall architecture, resilience and scalability of the whole product.

Caveats?

  • For total automation without manual interference during the initial creation we need to wait until Terraform and Pulumi have support for AppSync DataSources to Aurora Serverless and the Aurora Serverless Data API to become generally available.
  • Currently we can only do all this on AWS, but Terraform and Pulumi are cross cloud and we can reproduce this with other clouds once they have similar services available.

What are the alternatives?

The closest alternative would be to use AWS Amplify Framework. But I personally find it way too opinionated and creating way too much noise. Most importantly Amplify uses CloudFormation and this is an absolute no go for me (at the moment). CloudFormation takes away all your control and you are in Gods hands if anything goes wrong. So if you are dependent on CloudFormation you better be married to a high ranking AWS Account Manager and have an equally high ranking AWS Solution Architect as your mistress. Otherwise God help your poor soul. (CloudFormation experience deserves its own article or even an epic series of novels about war, pain and suffering).

As of May 2019 I am not aware of any other cloud solution that provides a managed GraphQL API that integrates with a serverless SQL Database. (Why this obsession with SQL? Because often NOSQL just doesn’t match your business data).

You can build your own solution by combining several different services from different providers, but this would be to messy and not realistic for enterprise solutions in my opinion.

You can build it yourself using containers, Kubernetes and other database solutions, but the amount of infrastructure you have to build and manage would be way more complex, fragile, insecure and expensive I would guess.

If you know something similar or even better to achieve this kind of BFF please leave a comment in the article. I am always keen to learn more.

Where to from here?

Now that we’ve got our BFF, we can use the identity pool to create and manage users. Navigate to the hosted sign-in page:

https://your_domain/login?response_type=code&client_id=your_app_client_id&redirect_uri=your_callback_url

This will redirect with an authentication code grant that you can exchange for user credentials using the authorization_code grant type:

With the user token you can then get access credentials from the identity pool (https://cognito-identity.us-east-1.amazonaws.com) which will allow you to access the AppSync GraphQL API as well as any other SigV4 signed AWS Service.

You first get an identity pool identity using your user pool credentials:

fetch(cognitoIdentityPoolUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-amz-json-1.1',
'X-Amz-Target': 'AWSCognitoIdentityService.GetId',
},
body: JSON.stringify({
'IdentityPoolId': identityPoolId,
'Logins': {
[`cognito-idp.${region}.amazonaws.com/${userPoolId}`]: cognitoUserPoolTokens.id_token,
},
}),

Then you can get access credentials:

fetch(cognitoIdentityPoolUrl, {
'method': 'POST',
'headers': {
'Content-Type': 'application/x-amz-json-1.1',
'X-Amz-Target': 'AWSCognitoIdentityService.GetCredentialsForIdentity',
},
'body': JSON.stringify({
IdentityId: cognitoIdentityPoolIdentityId,
'Logins': {
[`cognito-idp.${region}.amazonaws.com/${userPoolId}`]: cognitoUserPoolTokens.id_token,
},
}),

And with those you can access any AWS Service by signing your request via SigV4.

Here is a little app to test the cognito bit of your BFF:

Another advantage of this over Amplify is, that you are not limited to the languages and frameworks that Amplify supports. For example I use this with Dart in my Flutter mobile apps as well and it works just great.

Any more questions?

Please don’t hesitate and post questions and improvements or alternatives to this in the comment section.

Enjoy your new BFF ðŸ˜‰

Backend for frontend using AppSync

 


React Profiler: A Step by step guide to measuring app performance

  As react applications are becoming more and more complex, measuring application performance becomes a necessary task for developers. React...