繁体   English   中英

Azure Function 作为后端链接到 Azure Static Web 应用程序丢失 Azure AD AppRoleAssignments 声明

[英]Azure Function as linked backend to Azure Static Web App missing Azure AD AppRoleAssignments Claims

我们有一个 Static Web 应用程序,以及一个关联的 C# Function 应用程序(使用自带功能也称为“链接后端”方法)。 Static Web 应用程序和 Function 应用程序都与相同的 Azure AD 应用程序注册相关联。

当我们使用 Azure AD 和 go 对我们的 Static Web App: /.auth/me中的 auth 端点进行身份验证时,我们看到:

{
  "clientPrincipal": {
    "identityProvider": "aad",
    "userId": "d9178465-3847-4d98-9d23-b8b9e403b323",
    "userDetails": "johnny_reilly@hotmail.com",
    "userRoles": ["authenticated", "anonymous"],
    "claims": [
      // ...
      {
        "typ": "http://schemas.microsoft.com/identity/claims/objectidentifier",
        "val": "d9178465-3847-4d98-9d23-b8b9e403b323"
      },
      {
        "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
        "val": "johnny_reilly@hotmail.com"
      },
      {
        "typ": "name",
        "val": "John Reilly"
      },
      {
        "typ": "roles",
        "val": "OurApp.Read"
      },
      // ...
      {
        "typ": "ver",
        "val": "2.0"
      }
    ]
  }
}

注意那里的声明。 其中包括我们针对 Azure AD 应用注册配置的自定义声明,例如OurApp.Read的角色。

这样我们就可以在Static Web App(前端)中成功访问到理赔。 但是,关联的 Function 应用程序无权访问索赔。

可以通过在我们的 Azure 886982359588 应用程序中实现 function 来看到这一点,该应用程序显示角色:

[FunctionName("GetRoles")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "GetRoles")] HttpRequest req
)
{
    var roles = req.HttpContext.User?.Claims.Select(c => new { c.Type, c.Value });

    return new OkObjectResult(roles);
}

当访问此/api/GetRoles端点时,我们会看到:

[
  {
    "Type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
    "Value": "d9178465-3847-4d98-9d23-b8b9e403b323"
  },
  {
    "Type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
    "Value": "johnny_reilly@hotmail.com"
  },
  {
    "Type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
    "Value": "authenticated"
  },
  {
    "Type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
    "Value": "anonymous"
  }
]

乍一看,这似乎很棒; 我们有索赔。 但是当我们再次审视时,我们意识到我们的声明比我们希望的要少得多,至关重要的是,我们的自定义声明/应用程序角色(如OurApp.Read )丢失了。

答案改编自: https://blog.johnnyreilly.com/2022/11/17/azure-ad-claims-static-web-apps-azure-functions - 那里提供了更多上下文。

我们希望我们的 Azure Function 应用程序能够使用我们在 Static Web 应用程序中用于授权的相同自定义声明/应用程序角色。 我们怎样才能做到这一点?

微软图形 API

答案在于 Microsoft Graph API。我们可以查询它以获得用户的应用角色分配。 这将为我们提供与 Static Web 应用程序中相同的信息。 (严格准确地说,这将是一组略有不同的声明。但重要的是我们要用于授权的应用程序角色分配声明。)

我们已经有一个 Azure AD 应用程序注册。 为了我们可以查询 Microsoft Graph API,我们需要以下权限:

  • User.Read - 登录
  • User.Read.All - 用于获取针对用户的应用角色分配
  • Application.Read.All - 获取有关应用程序角色分配的更多信息 - 允许我们将应用程序角色分配转换为我们想要用于授权的声明

然后我们可以写如下:

using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.Graph;
using Microsoft.Identity.Client;

namespace MyApp.Auth
{
    public interface IAuthenticatedGraphClientFactory
    {
        (GraphServiceClient, string) GetAuthenticatedGraphClientAndClientId();
    }

    public class AuthenticatedGraphClientFactory : IAuthenticatedGraphClientFactory
    {
        private GraphServiceClient? _graphServiceClient;
        private readonly string _clientId;
        private readonly string _clientSecret;
        private readonly string _tenantId;

        public AuthenticatedGraphClientFactory(
            string clientId,
            string clientSecret,
            string tenantId
        )
        {
            _clientId = clientId;
            _clientSecret = clientSecret;
            _tenantId = tenantId;
        }

        public (GraphServiceClient, string) GetAuthenticatedGraphClientAndClientId()
        {
            var authenticationProvider = CreateAuthenticationProvider();

            _graphServiceClient = new GraphServiceClient(authenticationProvider);

            return (_graphServiceClient, _clientId);
        }

        private IAuthenticationProvider CreateAuthenticationProvider()
        {
            // this specific scope means that application will default to what is defined in the application registration rather than using dynamic scopes
            string[] scopes = new string[]
            {
                "https://graph.microsoft.com/.default"
            };

            var confidentialClientApplication = ConfidentialClientApplicationBuilder.Create(_clientId)
                .WithAuthority($"https://login.microsoftonline.com/{_tenantId}/v2.0")
                .WithClientSecret(_clientSecret)
                .Build();

            return new MsalAuthenticationProvider(confidentialClientApplication, scopes); ;
        }
    }

    public class MsalAuthenticationProvider : IAuthenticationProvider
    {
        private readonly IConfidentialClientApplication _clientApplication;
        private readonly string[] _scopes;

        public MsalAuthenticationProvider(IConfidentialClientApplication clientApplication, string[] scopes)
        {
            _clientApplication = clientApplication;
            _scopes = scopes;
        }

        /// <summary>
        /// Update HttpRequestMessage with credentials
        /// </summary>
        public async Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            var token = await GetTokenAsync();

            request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
        }

        /// <summary>
        /// Acquire Token
        /// </summary>
        public async Task<string?> GetTokenAsync()
        {
            var authResult = await _clientApplication.AcquireTokenForClient(_scopes).ExecuteAsync();

            return authResult.AccessToken;
        }
    }
}

我们将从我们的PrincipalService使用它:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Graph;

namespace MyApp.Auth
{
    public interface IPrincipalService
    {
        Task<ClaimsPrincipal> GetPrincipal(HttpRequest req);
    }

    public class PrincipalService : IPrincipalService
    {
        readonly ILogger<PrincipalService> _log;
        readonly IAuthenticatedGraphClientFactory _graphClientFactory;

        public PrincipalService(
            IAuthenticatedGraphClientFactory graphClientFactory,
            ILogger<PrincipalService> log
        )
        {
            _graphClientFactory = graphClientFactory;
            _log = log;
        }

        public async Task<ClaimsPrincipal> GetPrincipal(HttpRequest req)
        {
            try
            {
                MsClientPrincipal? principal = MakeMsClientPrincipal(req);

                if (principal == null)
                    return new ClaimsPrincipal();

                if (!principal.UserRoles?.Where(NotAnonymous).Any() ?? true)
                    return new ClaimsPrincipal();

                ClaimsIdentity identity = await MakeClaimsIdentity(principal);

                return new ClaimsPrincipal(identity);
            }
            catch (Exception e)
            {
                _log.LogError(e, "Error parsing claims principal");
                return new ClaimsPrincipal();
            }
        }

        MsClientPrincipal? MakeMsClientPrincipal(HttpRequest req)
        {
            MsClientPrincipal? principal = null;

            if (req.Headers.TryGetValue("x-ms-client-principal", out var header))
            {
                var data = header.FirstOrDefault();
                if (data != null)
                {
                    var decoded = Convert.FromBase64String(data);
                    var json = Encoding.UTF8.GetString(decoded);
                    _log.LogInformation($"x-ms-client-principal: {json}");
                    principal = JsonSerializer.Deserialize<MsClientPrincipal>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
                }
            }

            return principal;
        }

        async Task<ClaimsIdentity> MakeClaimsIdentity(MsClientPrincipal principal)
        {
            var identity = new ClaimsIdentity(principal.IdentityProvider);

            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, principal.UserId!));
            identity.AddClaim(new Claim(ClaimTypes.Name, principal.UserDetails!));

            if (principal.UserRoles != null)
                identity.AddClaims(principal.UserRoles
                    .Where(NotAnonymous)
                    .Select(userRole => new Claim(ClaimTypes.Role, userRole)));

            var username = principal.UserDetails;
            if (username != null)
            {
                var userAppRoleAssignments = await GetUserAppRoleAssignments(username);
                identity.AddClaims(userAppRoleAssignments
                    .Select(userAppRoleAssignments => new Claim(ClaimTypes.Role, userAppRoleAssignments)));
            }

            return identity;
        }

        static bool NotAnonymous(string r) =>
            !string.Equals(r, "anonymous", StringComparison.CurrentCultureIgnoreCase);

        async Task<string[]> GetUserAppRoleAssignments(string username)
        {
            try
            {
                var (graphClient, clientId) = _graphClientFactory.GetAuthenticatedGraphClientAndClientId();
                _log.LogInformation("Getting AppRoleAssignments for {username}", username);

                var userRoleAssignments = await graphClient.Users[username]
                    .AppRoleAssignments
                    .Request()
                    .GetAsync();

                var roleIds = new List<string>();
                var pageIterator = PageIterator<AppRoleAssignment>
                    .CreatePageIterator(
                        graphClient,
                        userRoleAssignments,
                        // Callback executed for each item in the collection
                        (appRoleAssignment) =>
                        {
                            if (appRoleAssignment.AppRoleId.HasValue && appRoleAssignment.AppRoleId.Value != Guid.Empty)
                                roleIds.Add(appRoleAssignment.AppRoleId.Value.ToString());

                            return true;
                        },
                        // Used to configure subsequent page requests
                        (baseRequest) =>
                        {
                            // Re-add the header to subsequent requests
                            baseRequest.Header("Prefer", "outlook.body-content-type=\"text\"");
                            return baseRequest;
                        });

                await pageIterator.IterateAsync();

                var applications = await graphClient.Applications
                    .Request()
                    .Filter($"appId eq '{clientId}'") // we're only interested in the app that we're running as
                    .GetAsync();

                var appRoleAssignments = applications
                    .FirstOrDefault()
                    ?.AppRoles
                    ?.Where(appRole => appRole.Id.HasValue && roleIds.Contains(appRole.Id!.Value.ToString()))
                    .Select(appRole => appRole.Value)
                    .ToArray();

                return appRoleAssignments ?? Array.Empty<string>();
            }
            catch (Exception e)
            {
                _log.LogError(e, "Error getting AppRoleAssignments");
                return Array.Empty<string>();
            }
        }

        class MsClientPrincipal
        {
            public string? IdentityProvider { get; set; }
            public string? UserId { get; set; }
            public string? UserDetails { get; set; }
            public IEnumerable<string>? UserRoles { get; set; }
        }
    }
}

然后,我们可以在 Azure Function 中使用PrincipalService获取一个 ClaimsPrincipal,其中包括我们的自定义声明/应用程序角色,如OurApp.Read 然后我们可以像我们希望的那样申请授权。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM