Dotnet-隔离的 Azure Functions - 如何访问 HttpContext

问题描述 投票:0回答:3

我有一个由 HTTP 调用触发的 dotnet 5 Azure Function(dotnet-isolated)。

该函数将由不同的 Azure 函数应用程序调用,我想使用 Azure AD 保护目标并使用“客户端凭据”OAuth2 流程。

我在以下位置找到了一个非常适合我的场景的示例:Microsoft GitHub Sample

我的问题是,该示例使用 WebApi 应用程序作为服务。这可以访问 HttpContext 对象,使用 Microsft.Identity.Web 程序集中名为“ValidateAppRole”的扩展方法

我的Azure函数确实有一个HttpRequestData类型的参数。它有一个包含键值对的 headers 属性。其中一个密钥称为“授权”,其值是 Azure AD 提供的访问令牌。我已通过 jwt.ms 运行此令牌,并且可以确认“角色”集合包含我需要验证的自定义角色。所以我知道所需的信息已经存在;我只是不知道如何以编程方式检查它。

由于点网隔离的 Azure 函数似乎无法访问 HttpContext 对象。如何进行相当于以下内容的检查?

HttpContext.ValidateAppRole("CustomRoleName");
oauth-2.0 azure-active-directory azure-functions microsoft-identity-web
3个回答
6
投票

我为此制作了一个示例函数应用程序:https://github.com/juunas11/IsolatedFunctionsAuthentication

该示例包含两个中间件:AuthenticationMiddleware 和 AuthorizationMiddleware。 前者验证 JWT 并从中创建 ClaimsPrincipal,而后者则根据 Function 方法上设置的属性检查 ClaimsPrincipal 上的声明。

您可以在 GitHub 存储库中查看完整代码。 身份验证中间件将 ClaimsPrincipal 设置为

FunctionContext
,其中:

context.Features.Set(new JwtPrincipalFeature(principal, token));

授权中间件可以通过以下方式从同一个上下文对象中获取它:

var principalFeature = context.Features.Get<JwtPrincipalFeature>();

代码随后可以检查特定声明。

Function 方法中也可以使用相同的上下文对象。 在示例中,属性与中间件一起使用,因此功能代码本身不必检查授权。


1
投票

Juunas 为使用中间件来做到这一点提供了很好的基线,对此我非常感激!

我已经更新了令牌身份验证中间件,以利用 .Net 7 中提供的新功能。我希望这可以帮助其他寻求指导的人!

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Middleware;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Worker.Http;
using System.Security.Claims;

namespace IdentityAccessTestIsolated.Middleware
{
    public class TokenAuthenticationMiddleware : IFunctionsWorkerMiddleware
    {
        private readonly JwtSecurityTokenHandler _tokenValidator;
        private readonly TokenValidationParameters _tokenValidationParameters;
        private readonly ConfigurationManager<OpenIdConnectConfiguration> _configurationManager;
        private readonly string _tenantId;
        public TokenAuthenticationMiddleware(IConfiguration configuration)
        {
            _tenantId = configuration["TenantId"];
            var audience = configuration["AuthenticationClientId"];
            _tokenValidator = new JwtSecurityTokenHandler();
            _tokenValidationParameters = new TokenValidationParameters
            {
                ValidAudience = audience
            };
            _configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>( 
                $"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration",
                new OpenIdConnectConfigurationRetriever());
        }

        public async Task Invoke(
            FunctionContext context,
            FunctionExecutionDelegate next)
        {
            var req = await context.GetHttpRequestDataAsync();

            if (!TryGetTokenFromHeaders(req, out var token))
            {
                // No token from headers
                var res = req!.CreateResponse(HttpStatusCode.Unauthorized);
                await res.WriteStringAsync("The supplied token was invalid");
                context.GetInvocationResult().Value = res;

                return;
            }

            if (!_tokenValidator.CanReadToken(token))
            {
                // Token is malformed
                var res = req!.CreateResponse(HttpStatusCode.Unauthorized);
                await res.WriteStringAsync("The supplied token was invalid");
                context.GetInvocationResult().Value = res;
                return;
            }

            // Get OpenID Connect metadata
            var validationParameters = _tokenValidationParameters.Clone();
            var openIdConfig = await _configurationManager.GetConfigurationAsync(default);
            validationParameters.ValidIssuer = openIdConfig.Issuer;
            validationParameters.ValidIssuer = openIdConfig.Issuer.Replace("{tenantid}", _tenantId); // manually modify the string to include our tenant
            validationParameters.IssuerSigningKeys = openIdConfig.SigningKeys;

            try
            {
                // Validate token
                var principal = _tokenValidator.ValidateToken(
                        token, validationParameters, out _);

                // Set principal + token in Features collection
                // They can be accessed from here later in the call chain
                context.Features.Set<ClaimsPrincipal>(principal);

                await next(context);
            }
            catch (Exception e)
            {
                // Token is not valid (expired etc.)
                var res = req!.CreateResponse(HttpStatusCode.Unauthorized);
                await res.WriteStringAsync("The supplied token was invalid");
                context.GetInvocationResult().Value = res;
                return;
            }
        }

        private static bool TryGetTokenFromHeaders(Microsoft.Azure.Functions.Worker.Http.HttpRequestData req, out string token)
        {
            token = null;
            // HTTP headers are in the binding context as a JSON object
            // The first checks ensure that we have the JSON string
            var headers = req.Headers;

            var authHeader = req.Headers.FirstOrDefault(a => a.Key.ToLowerInvariant() == "authorization").Value.FirstOrDefault();

            if (authHeader is null) return false;

            if (!authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
            {
                // Scheme is not Bearer
                return false;
            }

            token = authHeader.Substring("Bearer ".Length).Trim();
            return true;
        }
    }
}

0
投票

我可能找到了一种更简单的方法来实现其中一些目标。

让我知道你的想法..

//启动:

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication(worker =>
    {
        worker.UseMiddleware<FunctionsAuthenticationMiddleware>();
    })
    .ConfigureServices((context, services) =>
    {
        services.AddAuthentication(sharedOptions =>
        {
            sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddMicrosoftIdentityWebApi(context.Configuration).EnableTokenAcquisitionToCallDownstreamApi().AddInMemoryTokenCaches();

//中间件

public class FunctionsAuthenticationMiddleware : IFunctionsWorkerMiddleware
{
    private readonly IAuthenticationService _authenticationService;
    private readonly IServiceProvider _serviceProvider;

    public FunctionsAuthenticationMiddleware(IAuthenticationService authenticationService, IServiceProvider serviceProvider)
    {
        _authenticationService = authenticationService;
        _serviceProvider = serviceProvider;
    }


    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        // Extract the Authorization header
        var requestData = await context.GetHttpRequestDataAsync();

        // Extract the bearerToken
        var headers = requestData.Headers;
        var bearerToken = "";
        if (headers != null && headers.TryGetValues("Authorization", out var authorizationHeaders))
        {
            bearerToken = authorizationHeaders.FirstOrDefault();
        }

        //Create a new HttpContext (Because its impossible to get the current HttpContext in the new Azure Functions Isolated Worker Model)
        try
        {
            var httpContext = new DefaultHttpContext();
            httpContext.Request.Headers["Authorization"] = bearerToken;
            httpContext.RequestServices = _serviceProvider;
            var result = await _authenticationService.AuthenticateAsync(httpContext,"Bearer");
            if (result.Succeeded)
            {
                var user = result.Principal;

                var hasRandomRole = user.IsInRole("bananas.read");
© www.soinside.com 2019 - 2024. All rights reserved.