У нас есть статическое веб-приложение со связанным с ним приложением-функцией С# (используя подход «принеси свои собственные функции» AKA «связанный бэкэнд»). И статическое веб-приложение, и приложение-функция связаны с одной и той же регистрацией приложения Azure AD.
Когда мы аутентифицируемся в Azure AD и переходим к конечной точке аутентификации в нашем статическом веб-приложении: /.auth/me
мы видим:
{
"clientPrincipal": {
"identityProvider": "aad",
"userId": "d9178465-3847-4d98-9d23-b8b9e403b323",
"userDetails": "[email protected]",
"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": "[email protected]"
},
{
"typ": "name",
"val": "John Reilly"
},
{
"typ": "roles",
"val": "OurApp.Read"
},
// ...
{
"typ": "ver",
"val": "2.0"
}
]
}
}
Обратите внимание на претензии там. К ним относятся настраиваемые утверждения, которые мы настроили для нашей регистрации приложений Azure AD, например роли с OurApp.Read
.
Таким образом, мы можем успешно получить доступ к заявкам в статическом веб-приложении (внешнем интерфейсе). Однако связанное приложение-функция не имеет доступа к утверждениям.
Это можно увидеть, реализовав функцию в нашем приложении-функции Azure, которая отображает роли:
[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": "[email protected]"
},
{
"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 могло использовать те же пользовательские утверждения/роли приложения, которые мы используем для авторизации в статическом веб-приложении. Как мы можем этого добиться?
Ответ заключается в API Microsoft Graph. Мы можем опросить его, чтобы получить назначение роли приложения для пользователя. Это даст нам ту же информацию, что и в статическом веб-приложении. (Чтобы быть точным, это будет немного другой набор утверждений. Но важно то, что это будут утверждения о назначении роли приложения, которые мы хотим использовать для авторизации.)
У нас уже есть регистрация приложения Azure AD. Чтобы мы могли опрашивать API Microsoft Graph, нам потребуются следующие разрешения:
Затем мы можем написать следующее:
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; }
}
}
}
Затем мы можем использовать PrincipalService
внутри функции Azure, чтобы получить ClaimsPrincipal, который включает в себя наши пользовательские роли утверждений/приложений, такие как OurApp.Read
. Затем мы можем применить авторизацию, как мы могли бы надеяться, используя это.