[英]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 应用程序中用于授权的相同自定义声明/应用程序角色。 我们怎样才能做到这一点?
答案在于 Microsoft Graph API。我们可以查询它以获得用户的应用角色分配。 这将为我们提供与 Static Web 应用程序中相同的信息。 (严格准确地说,这将是一组略有不同的声明。但重要的是我们要用于授权的应用程序角色分配声明。)
我们已经有一个 Azure AD 应用程序注册。 为了我们可以查询 Microsoft Graph API,我们需要以下权限:
然后我们可以写如下:
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.