【问题标题】:Why is the Azure Function which is the linked backend to my Azure Static Web App missing Azure AD AppRoleAssignments Claims为什么作为我的 Azure 静态 Web 应用程序的链接后端的 Azure Function 缺少 Azure AD AppRoleAssignments 声明
【发布时间】:2022-11-18 00:04:42
【问题描述】:

我们有一个静态 Web 应用程序,以及一个关联的 C# 函数应用程序(使用自带函数又名“链接后端”方法)。静态 Web 应用程序和函数应用程序都与相同的 Azure AD 应用程序注册相关联。

当我们使用 Azure AD 进行身份验证并转到静态 Web 应用程序中的身份验证端点时:/.auth/me,我们看到:

{
  "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 的角色。

这样我们就可以在静态 Web 应用程序(前端)中成功访问声明。但是,关联的 Function App 确实不是有权访问索赔。

可以通过在我们的 Azure Function App 中实现一个显示角色的函数来看到这一点:

[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)丢失了。

【问题讨论】:

    标签: azure azure-active-directory azure-functions claims azure-static-web-app


    【解决方案1】:

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

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

    微软图形 API

    答案在于 Microsoft Graph API。我们可以询问它以获得用户的应用程序角色分配。这将为我们提供与静态 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 函数中使用 PrincipalService 获取一个 ClaimsPrincipal,其中包括我们的自定义声明/应用程序角色,如 OurApp.Read。然后我们可以像我们希望的那样申请授权。

    【讨论】:

      猜你喜欢
      • 2021-08-06
      • 1970-01-01
      • 2020-02-13
      • 2017-09-09
      • 2019-07-23
      • 2023-02-01
      • 1970-01-01
      • 2020-07-01
      • 1970-01-01
      相关资源
      最近更新 更多