Last week I was asked by someone in the community for a sample that shows how to secure Azure Functions with Azure AD. And specifically, a .NET 5 or 6 Azure Function using the Microsoft.Identity.Web library to validate tokens and authorize access.
Watch the video
If you want to watch the 1hr video that shows you how to do it end-to-end, it's available on YouTube
Grab the code
The sample project from the stream is available to clone from GitHub
Prerequisites
- You need an Azure Subscription Get it free here
- An Azure Active Directory Free M365 Developer
- .NET 6 Download here
- Identity extension tool for the .NET Core command line
- Azure Functions Core Tools Install instructions
Known issues
At the time of writing this, there is a known issue with .NET 6, Azure Functions **v4 and Microsoft.Identity.Web. Therefore, only the magic combination of the libries/NuGet packages mentioned in this blog/video work. The team is aware and working on a fix. What's the magic combo you ask? Your Azure Functions csproj
file should have these dependencies (hardcode the version!)
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.0.1" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
<PackageReference Include="Microsoft.Identity.Web" Version="1.5.1" />
</ItemGroup>
Create the project
The easiest way to get started is to use the .NET command line with the M.I.W extension (check the prereqs ^^^^). Open your favorite terminal and run the following command
mkdir <your directory name>
cd <your directory name>
dotnet new func2 --auth SingleOrg
This will create the files we need and implement the necessary middleware code.
First thing we need to do is open the *.csproj
file and update it to look like this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.0.1" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
<PackageReference Include="Microsoft.Identity.Web" Version="1.5.1" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
This will update the Azure Functions runtime to v4, set the target framework to .NET 6 and then pin the NuGet packages to the versions that work!
That's all the changes we have to implement in the code.
Configure Azure Active Directory
The Azure function is already configured to expect authenticated calls. Out of the box, since we used the M.I.W extension, there is also an API permission that we need to configure. Now, you can go ahead and set up Azure AD manually, or you can use the nifty .NET Notebook in the project to programmatically set up everything for you.
Configure the Azure AD settings in the Function project
Open appsettings.json
and add the App Registration information that was in the output of the Notebook from the previous step. Your appsettings.json
should look like this:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "<your ad domain name>.onmicrosoft.com",
"TenantId": "<your tenant id>",
"ClientId": "<your API Client Id>",
"CallbackPath": "/signin-oidc"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Save and close! You are now ready to run the Azure Function! In your terminal, type func host start
. You should be presented with the following output that indicates that your Azure Function is up and running:
The default endpoint to call the API is: http://localhost:7071/api/SampleFunc
How does authentication work in this code?
Firstly, in Startup.cs
we configure our authentication middleware in the ConfigureServices()
method with the following code:
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = Microsoft.Identity.Web.Constants.Bearer;
sharedOptions.DefaultChallengeScheme = Microsoft.Identity.Web.Constants.Bearer;
})
.AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
Then, inside the Function code, first we make sure that we have an authenticated request, i.e. we look for an auth header with Bearer tokenstring
value. If there is no Auth header, the code will generate an HTTP 401 error. Secondly, we check the token claims to ensure that the user has the right api permission, in this instance access_as_user
. If a valid token with the wrong permission or missing permissions is presented, the code will return a 403 error back to the client. If a valid token and the right permissions are present, the code executes as expected. The code that does this is here:
var (authenticationStatus, authenticationResponse) =
await req.HttpContext.AuthenticateAzureFunctionAsync();
if (!authenticationStatus) return authenticationResponse;
req.HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
NOTE: Auth attributes (a la ASP.NET) are supported but currently ignored. There will be a fix soon to fully support method-level attributes
Call the Azure Function from your client
Whichever client you use, you need to make sure that you can authenticate and get an Access Token to pass to your Azure Function. I use Thunder Client, which is a VS Code extension. These are my settings:
We can now call our Function from our client. If everything was setup correctly, we should receive an HTTP 200 with a response that contains a greeting with the name of the signed in user.
We now have an HTTP endpoint built with .NET 6 and Azure Functions, all secured with Azure Active Directory in 3 lines of code!
Summary
There may still be a few small issues with the Microsoft.Identity.Web library and Azure Functions, but if you're willing to take a hard dependency on a couple of NuGet packages, then you get a lot for free using the out of the box tools. As always, make sure to reach out to us via Discord or email us at 425Show@microsoft.com if you have any questions!
Top comments (3)
This worked the first try. I can see the user display name, the user ObjectID and the TenantID from within the Azure function. The access token was sent from a desktop app I had already created.
Outstanding!
Functionsv4 does not have a startup.cs
Why did you not use AuthorizationLevel.User in the function?