Azure Functions — Validate Azure active directory tokens using your own custom binding

Cheranga Hatangala
Cheranga
Published in
6 min readMay 9, 2021

--

Azure functions are great! but as in many frameworks there are times where you would like to extend or write your own custom logic to do what you are intending to do. It’s not about re-inventing the wheel of course, but how to use the underlying framework’s extension points so that you can build on top of that. So in this article let’s design and implement an Azure custom binding where you can use to validate JWT tokens issued by Azure Active Directory.

What are we trying to achieve?

Imagine that you are implementing an HTTP triggered Azure function and, you would like to secure it using AAD (Azure Active Directory)

Unlike ASP.NET Core Web APIs you don’t have rich features like Authorization Filters in Azure functions. So to authenticate and authorize your endpoints you will need to write code to handle that.

Here are some of the options which we could use,

  • Implement a service to perform token validation and inject it as a dependency to your azure functions.
  • Deploy the azure function behind an APIM and add JWT validation policies.
  • Create our own custom Azure binding

Although in my opinion the 2nd option is always something which we must do anyway. Implementing a service and injecting will be great, but if you have multiple Azure functions (either HTTP or some other trigger) implemented in the same class some of the Azure functions may not need authentication/authorization. But still it’s a good option.

In this article we’ll implement a custom Input Azure Binding which it will validate a token using AAD and will make available the ClaimsPrincipal from the token to be used inside the function if required.

NOTE: With Azure functions with .NET 5 you can implement your own custom middleware like in ASP.NET Core (Will post an article about this in a later post). This articles focuses on Azure functions (V3) which are developed using .NET Core 3.x

The Azure function which we are going to secure

To make this article focused on the custom input binding and the JWT validation let’s have the simplest HTTP triggered Azure function.

“It will do a fake 2 second delay and will return a string”

As you can see the functions authorization level is set to anonymous so that we can control how the authentication/authorization is done.

Implementing a custom input Azure function binding

In here we would like to develop an input binding so that if the token is validated it will have a valid ClaimsPrincipal for us to use inside the Azure function.

  • Step 1 (Create the Attribute so various settings can be passed)

All the properties are decorated with the AutoResolve attribute so that these settings can be populated from the function configuration. The AuthorizeUrl is set to the well known url where it will be used to load the openid configuration data from AAD.

  • Step 2 (Implement the binding and letting Azure function runtime know about it)

By using IExtensionConfigProvider you can allow the Azure function runtime to use your code when it executes. This happens automatically in Azure function apps which are from V2 or above.

In the preceding code it instructs the Azure function runtime to use the AzureAdTokenAttribute to populate the AzureAdToken instance.

  • Step 3 (Implement the token validation)

Let’s separate the token validation logic from the custom binding. As per the code above all the required configuration data to perform a token validation are inside the AzureAdTokenAttribute instance. But the token will be inside the HttpRequest . To have access to that when we implement the token validation service we’ll inject the IHttpContextAccessor . You don’t want to register this as it will be provided by the Azure function runtime itself.

In the code above these are the actions it performs,

  • It exposes a method called GetClaimsPrincipalAsync which accepts an AzureAdTokenAttribute instance.
  • From the injected IHttpContextAccessor it will get the Bearer token data from the Authorization HTTP header data.
  • It will use the passed AzureAdTokenAttribute configuration to get the OpenIdConfigurationData from the AAD so that it can validate the JWT token in AAD.
  • It uses the registered ISecurityTokenValidator from the Microsoft.IdentityModel.Tokens library to validate the token (you’ll see that we will be registering this dependency with JwtSecurityTokenHandler from the System.IdentityModel.Tokens.Jwt library in the Startup class later).
  • To validate the token parameters it will use TokenValidationParameters and below are the properties used to validate the JWT token.
var validationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
RequireSignedTokens = true,
ClockSkew = TimeSpan.Zero,

ValidAudience = azureAdData.Audience,

IssuerSigningKeys = openIdConnectConfigData.SigningKeys,
ValidIssuer = openIdConnectConfigData.Issuer
};
  • Once the validation is performed and, if it’s valid then it’ll further check if any of the roles and scopes are available in the token. This is done only if they are defined in the passed AzureAdTokenAttribute itself.
  • If the token validation was successful then it will return the ClaimsPrincipal obtained from AAD. Otherwise it will return a null (not a big fan of returning null or throwing exceptions but in this case it makes sense to return null because actually if the token is invalid there will be no ClaimsPrincipal ).

Now use this service in your custom binding implementation,

If the attribute data is not passed it will read it from the configuration data as shown in the GetAttributeWithRequiredSettings method.

  • Step 4 (Implement an extension method so that the binding and it’s related dependencies can be registered in one place)

Most important to notice in here is that, this is an extension of the IWebJobsBuilder not the IFunctionsHostBuilder . The reason being we cannot register extensions using the AddExtension method being made available from the Azure function SDK (at least I couldn’t find a way, please let me know if we can do so).

the codebuilder.AddExtension<AzureAdTokenBinding> will register your custom binding with the Azure function runtime. Also as you can see the other dependencies which are used inside the custom binding are registered there.

  • Step 5 (Let’s use the extension method in our startup)

As you can see the Startup class implements IWebJobsStartup and registers the binding through the extension method.

  • Step 6 (Updating the configuration / local.settings.json)
{
"IsEncrypted": false,
"Values": {
"AzureAd:TenantId": "[YOUR TENANT ID]",
"AzureAd:ClientId": "[THE CLIENT ID OF THE APP WHICH USES THE SERVICE]",
"AzureAd:Instance": "https://login.microsoft.com/",
"AzureAd:Audience": "[THE APPLICATION ID OF THE SERVICE WHICH THE CLIENT IS ACCESSING]",
"AzureAd:Scopes": "access.bff, test.bff",
"AzureAd:Roles": "sales.bff.orders.search",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
}
}

Using the custom binding in your Azure function

Now finally, let’s use the custom binding which we implemented in our HTTP triggered Azure function.

  • Use case 1 (we just need a valid token)

You just need to add an AzureAdToken and decorate it with the AzureAdTokenAttribute . Since we just need to have a valid token we don’t need to pass any other parameters.

  • Use case 2 (Specific roles and scopes)

If you just need either roles or scope, just pass an empty string or a null as the parameter.

  • Use case 3 (Want to load configurations from other configured sections)

In the preceding code we will be loading the required data from different sections in the configuration like SomeOtherSection and AnotherSection (I am sure that you can come up with better names than me 😆)

The code for this can be found in GitHub

Conclusion

Using Azure functions custom binding feature we can extend and implement our own features quite easily. Please feel free to add your inputs, comments, critics here.

Thank you!

References

Please refer the README in the repository for references and fixes which you may encounter.

--

--