Search code examples
azurefunctionapiauthenticationauth0

Azure Functions app + Auth0 provider, getting 401 when calling API with auth token


I have read, and implemented local dev projects to match, Auth0's Complete Guide To React User Authentication with Auth0, successfully. I am confident in the implementation, given that all aspects of login and route protection are working correctly, as well as the local express server successfully authenticating API calls that use authentication tokens generated via the Auth0 React SDK.

I have added third button to the sample project's external-apis.js view for use in calling another API that I am trying to integrate with, which is an Azure Functions app. I would like to use Auth0 for this API in the same way I do for the express server, and take advantage of Azure's "Easy Auth" capabilities, as discussed in this MS doc. I have implemented an OpenID Connect provider, which points to my Auth0 application, in my Azure Function app per this MS doc.

This is what the function that calls this Azure Function app API looks like:

  const callAzureApi = async () => {
    try {
      const token = await getAccessTokenSilently();
      await fetch(
        'https://example.azurewebsites.net/api/ExampleEndPoint',
        {
          method: 'GET',
          headers: {
            'content-type': 'application/json',
            authorization: `Bearer ${token}`,
          },
        }
      )
        .then((response) => response.json())
        .then((response) => {
          setMessage(JSON.stringify(response));
        })
        .catch((error) => {
          setMessage(error.message);
        });
    } catch (error) {
      setMessage(error.message);
    }
  };

My issue is that making calls to this Azure Function app API always returns a 401 (Unuthorized) response, even though the authorization token is being sent. If I change the Authorization settings in the Azure portal to not require authentication, then the code correctly retrieves the data, so I'm confident that the code is correct.

But, is there something else I have missed in my setup in order to use Auth0 as my authentication provider for the backend in Azure?


Solution

  • Through continued documentation and blog reading, I was able to determine what was missing from my original implementation. In short, I was expecting a little too much after reading about tge "Easy Auth" features of Azure, at least when using an OpenID Connect provider like Auth0. Specifically, the validation of the JSON Web Token (JWT) does not come for free, and needed further implementation.

    My app is using the React Auth0 SDK to sign the user in to the identity provider and get an authorization token to send in its API requests. The Azure documentation for client-directed sign-in flow discusses the ability to validate a JWT using a specific POST call to the auth endpoint with the JWT in the header, but even this feature seems out of reach here, given that OpenID Connect is not listed in the provider list, and my attempts at trying it anyway continued to yield nothing but 401s.

    The answer, then, was to implement the JWT validation directly into the Azure function itself, and return the proper response only when the JWT in the request header can be validated. I would like to credit blog posts of Boris Wilhelm and Ben Chartrand for helping to get to this final understanding of how to properly use Auth0 for an Azure Functions backend API.

    I created the following Security object to perform the token validation. The static nature of the ConfigurationManager is important for caching the configuration to reduce HTTP requests to the provider. (My Azure Functions project is written in C#, as opposed to the React JS front-end app.)

    using System;
    using System.IdentityModel.Tokens.Jwt;
    using System.Net.Http.Headers;
    using System.Security.Claims;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.IdentityModel.Protocols;
    using Microsoft.IdentityModel.Protocols.OpenIdConnect;
    using Microsoft.IdentityModel.Tokens;
    
    namespace ExampleProject.Common {
        public static class Security {
            private static readonly IConfigurationManager<OpenIdConnectConfiguration> _configurationManager;
            private static readonly string ISSUER = Environment.GetEnvironmentVariable("Auth0Url", EnvironmentVariableTarget.Process);
            private static readonly string AUDIENCE = Environment.GetEnvironmentVariable("Auth0Audience", EnvironmentVariableTarget.Process);
    
            static Security()
            {
                var documentRetriever = new HttpDocumentRetriever {RequireHttps = ISSUER.StartsWith("https://")};
    
                _configurationManager = new ConfigurationManager<OpenIdConnectConfiguration> (
                    $"{ISSUER}.well-known/openid-configuration",
                    new OpenIdConnectConfigurationRetriever(),
                    documentRetriever
                );
            }
    
            public static async Task<ClaimsPrincipal> ValidateTokenAsync(AuthenticationHeaderValue value) {
                if(value?.Scheme != "Bearer")
                    return null;
    
                var config = await _configurationManager.GetConfigurationAsync(CancellationToken.None);
    
                var validationParameter = new TokenValidationParameters {
                    RequireSignedTokens = true,
                    ValidAudience = AUDIENCE,
                    ValidateAudience = true,
                    ValidIssuer = ISSUER,
                    ValidateIssuer = true,
                    ValidateIssuerSigningKey = true,
                    ValidateLifetime = true,
                    IssuerSigningKeys = config.SigningKeys
                };
    
                ClaimsPrincipal result = null;
                var tries = 0;
    
                while (result == null && tries <= 1) {
                    try {
                        var handler = new JwtSecurityTokenHandler();
                        result = handler.ValidateToken(value.Parameter, validationParameter, out var token);
                    } catch (SecurityTokenSignatureKeyNotFoundException) {
                        // This exception is thrown if the signature key of the JWT could not be found.
                        // This could be the case when the issuer changed its signing keys, so we trigger
                        // a refresh and retry validation.
                        _configurationManager.RequestRefresh();
                        tries++;
                    } catch (SecurityTokenException) {
                        return null;
                    }
                }
    
                return result;
            }
        }
    }
    

    Then, I added this small bit of boilerplate code toward the top of any HTTP-triggered functions, before any other code is run to process the request:

    ClaimsPrincipal principal;
    if ((principal = await Security.ValidateTokenAsync(req.Headers.Authorization)) == null) {
        return new UnauthorizedResult();
    }
    

    With this in place, I finally have the implementation I was looking for. I'd like to improve the implementation with something more generic like a custom attribute, but I'm not sure that's possible yet either for OpenID Connect providers. Still, this is a perfectly acceptable solution for me, and gives me the level of security I was looking for when using a React front-end with an Azure Functions back-end.

    Cheers!