Azure Functions 应用程序 + Auth0 提供程序,使用身份验证令牌调用 API 时获得 401

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

我已成功阅读并实施本地开发项目以匹配 Auth0 的 Complete Guide To React User Authentication with Auth0。我对实施充满信心,因为登录和路由保护的所有方面都正常工作,并且本地快速服务器成功验证了 API 使用通过 Auth0 React SDK 生成的身份验证令牌的调用。

我已将第三个按钮添加到示例项目的外部-apis.js 视图,用于调用另一个我尝试与之集成的 API,这是一个 Azure Functions 应用程序。我想像对 express 服务器一样对这个 API 使用 Auth0,并利用 Azure 的“Easy Auth”功能,如 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.

所讨论的

调用此 Azure Function 应用程序的函数如下所示 API:

  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);
    }
  };

我的问题是调用此 Azure Function 应用程序 API 总是 returns 401(未授权)响应,即使正在发送授权令牌。如果我将 Azure 门户中的授权设置更改为不需要身份验证,则代码会正确检索数据,因此我确信代码是正确的。

但是,为了将 Auth0 用作 Azure 后端的身份验证提供程序,我的设置中是否还遗漏了其他内容?

通过继续阅读文档和博客,我能够确定我的原始实现中缺少什么。简而言之,在阅读了 Azure 的 tge“Easy Auth”功能后,我的期望有点过高,至少在使用像 Auth0 这样的 OpenID Connect 提供商时是这样。具体来说,JSON Web 令牌 (JWT) 的验证不是免费的,需要进一步实施。

我的应用程序使用 React Auth0 SDK 将用户登录到身份提供者并获取授权令牌以发送其 API 请求。 client-directed sign-in flow 的 Azure 文档讨论了使用 header 中的 JWT 对 auth 端点的特定 POST 调用来验证 JWT 的能力,但即使是此功能在这里似乎也无法实现,鉴于 OpenID Connect 未列在提供者列表中,我尝试尝试它仍然只产生 401s。

然后,答案是直接在 Azure 函数本身中实施 JWT 验证,return 只有当请求中的 JWT header 可以被验证时,才是正确的响应。我想感谢 Boris Wilhelm and Ben Chartrand 的博文帮助我最终理解如何为 Azure Functions 后端正确使用 Auth0 API。

我创建了以下安全性 object 来执行令牌验证。 ConfigurationManager 的静态特性对于缓存配置以减少对提供程序的 HTTP 请求很重要。 (我的 Azure Functions 项目是用 C# 编写的,而不是 React JS front-end 应用程序。)

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;
        }
    }
}

然后,我在任何 HTTP-triggered 函数的顶部添加了一小段样板代码,在任何其他代码 运行 处理请求之前:

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

有了这个,我终于有了我正在寻找的实现。我想用更通用的东西(如自定义属性)改进实现,但我不确定这对 OpenID Connect 提供商是否可行。尽管如此,这对我来说仍然是一个完全可以接受的解决方案,并且提供了我在将 React front-end 与 Azure Functions back-end.

一起使用时所寻求的安全级别

干杯!