6 minute read

I recently run into an interesting situation with authentication. Since serverless and Azure Functions are gaining more and more traction, I wanted to see if it’s possible to make use of Microsoft.Identity.Web library from within Azure Functions… and it worked!

Initially, my requirement was to support both Azure AD and static API key authentication for the same endpoints. In ASP.NET Core, this is done simply by setting up a policy scheme via .AddPolicyScheme, with Azure functions, it is slightly different.

First off, Azure Functions don’t have any concept of middlewares, except for filters which are at the time of writing in preview, so this is probably not really something you want to go with.

Azure Functions however support regular dependency injection, so you can add your custom services. One of the services for example is AddJwtBearer brought to you by Microsoft.AspNetCore.Authentication.JwtBearer package. Instead of using playn JwtBearer middleware, we are going to use Microsoft.Identity.Web (MIW). The cool thing about MIW is, that it simplifies most of the common actions and operations you need to do. It also makes usage of Microsoft Graph or calling any other API super simple!

So in order to add it, all you need to do is to create a Startup.cs file which looks like this:

[assembly: FunctionsStartup(typeof(FunctionsAuthentication.Startup))]
namespace FunctionsAuthentication
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            // This is configuration from environment variables, settings.json etc.
            var configuration = builder.GetContext().Configuration;

            builder.Services.AddAuthentication(sharedOptions =>
            {
                sharedOptions.DefaultScheme = "Bearer";
                sharedOptions.DefaultChallengeScheme = "Bearer";
            })
                .AddMicrosoftIdentityWebApi(configuration)
                    .EnableTokenAcquisitionToCallDownstreamApi()
                    .AddInMemoryTokenCaches();
        }
    }
}

Don’t forget to add Microsoft.Identity.Web package and configure your local.settings.json (it should probably end up looking like this):

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "AzureAd:Instance": "https://login.microsoftonline.com/",
    "AzureAd:Domain": "<your_domain>",
    "AzureAd:TenantId": "<your_tenant_id>",
    "AzureAd:ClientId": "<client_id>",
    "AzureAd:ClientSecret": "<client_secret>"
  }
}

Now we have to make a little HttpContext extension, I will explain what it does below:

public static class FunctionsAuthenticationHttpContextExtension
{
    public static async Task<(bool, IActionResult)> AuthenticateFunctionAsync(this HttpContext httpContext, string schemaName)
    {
        var result = await httpContext.AuthenticateAsync(schemaName);
        if (result.Succeeded)
        {
            httpContext.User = result.Principal;
            return (true, null);
        }
        else
        {
            return (false, new UnauthorizedObjectResult(new ProblemDetails
            {
                Title = "Authorization failed.",
                Detail = result.Failure?.Message
            }));
        }
    }
}

This extension method is going to be called whenever the Function is invoked, because you can’t make any direct use of regular [Authorize] like in ASP.NET Core. This extension method does three things:

  • Attempts to authenticate the principal via the provided schemaName
  • If authentication succeeds, it populates HttpContext.User with the principal information like claims (it will also make User.Identity.IsAuthenticated work etc.)
  • If authentication fails, produces an IActionResult with the error message (see Problem Details for more information).

Thanks to HttpContext.User containing correct principal, you can also make use of VerifyUserHasAnyAcceptedScope to easily validate the scope passed in the token.

Next we proceed with the function. First, make it a regular non-static class, so we can make use of ITokenAcquisition (will need it for later). Next, make sure the function’s authorization level is set to anonymous ([HttpTrigger(AuthorizationLevel.Anonymous, ...]), otherwise you will run into following issue: Azure Functions use multiple authentication services and authorization level triggers them, so you would end up with errors like missing scheme or similar.

Next, you simply call the AuthenticateFunctionAsync with the schema name, the default is Bearer (from JwtBearerDefaults), but you can replace it with your own policy’s name. The extension method returns a tuple, first value is a boolean - whether authentication was ok or not and second is the desired IActionResult which you can return if the authentication failed.

If you plan to use the on-behalf-of flow to obtain a token for another service with the current user’s token (eg. Microsoft Graph) you also want to inject ITokenAcquisition into the constructor (this is another reason to populate HttpContext.User since ITokenAcquisition uses the principal for token caching). In the code above, we called AddInMemoryTokenCaches which you may argue is ineffective for Functions, since it’s just in memory and not shared. That’s true, you can opt-in to move to distributed token cache in a single line of code (there are implementations for Redis, SQL, Cosmos and Table Storage). The rest is just as easy as to call GetAccessTokenForUserAsync on ITokenAcquisition and if the scopes have been granted correctly, you will get a token (try calling it again and diffcheck the token, to check if token caching works). If you add the Microsoft Graph package, you can easily call the GraphServiceClient as well.

And now, the function code itself:

public class Function1
{
    private readonly ITokenAcquisition _tokenAcquisition;
    public Function1(ITokenAcquisition tokenAcquisition)
    {
        _tokenAcquisition = tokenAcquisition;
    }

    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        var (authenticationStatus, authenticationResponse) = await req.HttpContext.AuthenticateFunctionAsync("Bearer");
        if (!authenticationStatus) return authenticationResponse;

        var token = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "https://graph.microsoft.com/.default" });

        log.LogInformation("C# HTTP trigger function processed a request.");

        string name = req.HttpContext.User.Identity.IsAuthenticated ? req.HttpContext.User.Identity.Name : null;

        string responseMessage = string.IsNullOrEmpty(name)
            ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
            : $"Hello, {name}. This HTTP triggered function executed successfully.";

        return new OkObjectResult(responseMessage);
    }
}

So far, I found this to be quite fulfilling, since it doesn’t seem to lack many things I am used to from ASP.NET Core (except for the attributes). Give it a try and see for yourself!

Also, please note, that like Christos mentioned MIW wasn’t made to work with Functions, so it’s not likely to receive any official support.

Update: Thanks to jennyf19 for confirming, that the team is interested in the scenario and will help out with any issues you may encounter along the way! Simply create an issue in the repo if needed.

Comments

Alexander Tuttle

Hi Jan,

Thanks for a great post. I have some questions regarding this:

  1. Is it true that the above currently only works when running locally because of this: https://github.com/AzureAD/microsoft-identity-web/issues/916 ?

  2. When deploying, should the values in local.settings.json be added to host.json or should they be added as Application Settings for the function App Service?

  3. …and in general, when adding a NuGet-package to an Azure Function, such as Microsoft.Identity.Web, that requires configuration settings, how do I know where to put the configuration values?

Would really appreciate if you could answer.

Regards,

Alexander Tuttle

Jan Hajek

Hey, great points:

  1. It works also in Azure (in fact I am using it in production), but you loose some of the portal integration (like log streaming), but everything works well with App Insights.
  2. In this case, I use local.settings.json for local development and Application Settings (Environment Variables) for Azure hosting.
  3. The configuration is set via Startup.cs during service registration into the service provider (dependency injection). The configuration is defined in Microsoft.Identity.Web’s docs. The available configuration is for example shown here: https://github.com/AzureAD/microsoft-identity-web/wiki/web-apis
saurabh

i am getting error - Microsoft.AspNetCore.Authentication.Core: No authentication handler is registered for the scheme ‘WebJobsAuthLevel’. The registered schemes are: B2C. Did you forget to call AddAuthentication().AddSomeAuthHandler?.

Please help ..

Jan Hajek

Great point @saurabh. This is a known issue, partially covered here: https://github.com/AzureAD/microsoft-identity-web/issues/916. To mitigate this error on startup, make sure that all your HTTP triggers are set to Anonymous level. Eventually, you could also try following: https://hajekj.net/2021/04/22/azure-functions-out-of-process-and-authentication-with-azure-ad/ to make other authorization types work as well (like the Functions key). You can use the same code with in-process hosting model as well.

Kiran

This is not working. All my http triggers are at Anonymous level. I get error as below. Until there is a clean solution, I feel it is better to display some warning at the beginning of this post so future readers do not spend their time on this approach.

[2021-08-22T16:13:48.888Z] An unhandled host error has occurred. [2021-08-22T16:13:48.893Z] Microsoft.AspNetCore.Authentication.Core: No authentication handler is registered for the scheme ‘ArmToken’. The registered schemes are: Bearer. Did you forget to call AddAuthentication().AddSomeAuthHandler?.

Yomodo

Thank you soo much for this article, it helped us out a lot!

Unfortunately we eventually ran in to an issue regarding AddMicrosoftIdentityWebApi, that seems to be a bug:

https://github.com/AzureAD/microsoft-identity-web/issues/1548

Gavin

Thanks for the article, really useful and good to confirm that what we are trying to achieve isn’t completely crazy, just seemingly under supported. Is this still the best way to achieve AD in functions without Azure portal on 2023??

To submit comments, go to GitHub Discussions.