in src/Saas.Identity/Saas.Permissions/Saas.Permissions.Service_v1.1/Services/GraphAPIService.cs [12:244]
public class GraphAPIService(
IOptions<AzureB2CPermissionsApiOptions> permissionApiOptions,
IGraphApiClientFactory graphClientFactory,
ILogger<GraphAPIService> logger) : IGraphAPIService
{
private readonly ILogger _logger = logger;
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/loggermessage?view=aspnetcore-8.0
private static readonly Action<ILogger, Exception> _logError = LoggerMessage.Define(
LogLevel.Error,
new EventId(1, nameof(GraphAPIService)),
"Client Assertion Signing Provider");
private readonly GraphServiceClient _graphServiceClient = graphClientFactory.Create();
private readonly AzureB2CPermissionsApiOptions _permissionOptions = permissionApiOptions.Value;
public async Task<string[]> GetAppRolesAsync(ClaimsRequest request)
{
try
{
if (request.ClientId is null)
{
throw new NullReferenceException("Client ID cannot be null.");
}
ServicePrincipal? servicePrincipal = await GetServicePrincipalAsync(request.ClientId);
return servicePrincipal is null
? throw new ArgumentException($"App role not found for \"{request.ClientId}\".")
: await GetAppRoleAssignmentsAsync(servicePrincipal, request.ObjectId.ToString());
}
catch (Exception ex)
{
_logError(_logger, ex);
throw;
}
}
public async Task<Models.User> GetUserByEmail(string userEmail)
{
try
{
var graphUsers = await _graphServiceClient.Users
.GetAsync(requestionConfiguration =>
{
requestionConfiguration.QueryParameters.Filter = $"identities/any(id: id/issuer eq '{_permissionOptions.Domain}' and id/issuerAssignedId eq '{userEmail}')";
requestionConfiguration.QueryParameters.Select = new string[] { "id, identities, displayName" };
});
if (graphUsers?.Value?.Count > 1)
{
throw new UserNotFoundException($"More than one user with the email {userEmail} exists in the Identity provider");
}
if (graphUsers?.Value?.Count == 0 || graphUsers?.Value is null)
{
throw new UserNotFoundException($"The user with the email {userEmail} was not found in the Identity Provider");
}
// Ok to just return first, because at this point we've verified we have exactly 1 user in the graphUsers object.
return ToUserObjects(graphUsers.Value).First();
}
catch (Exception ex)
{
_logError(_logger, ex);
throw;
}
}
// Enriches the user object with data from Microsoft Graph.
public async Task<Models.User> GetUserById(Guid userId)
{
try
{
var graphUsers = await _graphServiceClient.Users
.GetAsync(requestionConfiguration =>
{
requestionConfiguration.QueryParameters.Filter = $"id eq '{userId}'";
requestionConfiguration.QueryParameters.Select = new string[] { "id, identities, displayName" };
});
if (graphUsers?.Value?.Count > 1)
{
throw new UserNotFoundException($"More than one user with the id {userId} exists in the Identity provider");
}
if (graphUsers?.Value?.Count == 0 || graphUsers?.Value is null)
{
throw new UserNotFoundException($"The user with the id {userId} was not found in the Identity Provider");
}
// Ok to just return first, because at this point we've verified we have exactly 1 user in the graphUsers object.
return ToUserObjects(graphUsers.Value).First();
}
catch (Exception ex)
{
_logError(_logger, ex);
throw;
}
}
// Enriches the user object with data from Microsoft Graph.
public async Task<IEnumerable<Models.User>> GetUsersByIds(ICollection<Guid> userIds)
{
List<Models.User> userList = new();
try
{
var graphUsers = await _graphServiceClient.Users
.GetAsync(requestConfiguration =>
{
requestConfiguration.QueryParameters.Filter = MakeUserFilter();
});
if (graphUsers?.Value is null)
{
return userList;
}
PageIterator<Microsoft.Graph.Models.User, UserCollectionResponse> pageIterator
= PageIterator<Microsoft.Graph.Models.User, UserCollectionResponse>
.CreatePageIterator(
_graphServiceClient,
graphUsers,
(msg) =>
{
userList.Add(ToUserObject(msg));
return true;
});
await pageIterator.IterateAsync();
return userList;
}
catch (Exception ex)
{
_logError(_logger, ex);
throw;
}
string MakeUserFilter ()
{
// Build graph query: "id in ('id1', 'id2')"
// https://docs.microsoft.com/en-us/graph/aad-advanced-queries?tabs=csharp
StringBuilder filter = new();
filter.Append("id in (");
filter.Append(string.Join(",", userIds.Select(id => $"'{id}'")));
filter.Append(')');
return filter.ToString();
}
}
private async Task<ServicePrincipal?> GetServicePrincipalAsync(string clientId)
{
try
{
var servicePrincipal = await _graphServiceClient.ServicePrincipals
.GetAsync(requestConfiguration =>
{
requestConfiguration.QueryParameters.Filter = $"appId eq '{clientId}'";
});
return servicePrincipal?.Value?.SingleOrDefault();
}
catch (Exception ex)
{
_logError(_logger, ex);
throw;
}
}
private async Task<string[]> GetAppRoleAssignmentsAsync(ServicePrincipal servicePrincipal, string userId)
{
try
{
var userAppRoleAssignments = await _graphServiceClient.Users[userId].AppRoleAssignments
.GetAsync(requestConfiguration =>
{
requestConfiguration.Equals($"resourceId eq {servicePrincipal.Id}");
}) ?? throw new ArgumentException($"App role not found for \"{servicePrincipal.AppId}\".");
var appRoleIds = userAppRoleAssignments?.Value?
.Where(appRole => appRole is not null)
.Where(appRole => appRole.AppRoleId is not null)
.Select(appRole => appRole.AppRoleId);
if (appRoleIds is null || !appRoleIds.Any())
{
throw new ArgumentException($"App role not found for \"{servicePrincipal.AppId}\".");
}
var appRoles = servicePrincipal.AppRoles?
.Where(appRole => appRole?.Id is not null)
.Where(appRole => appRoleIds.Contains(appRole.Id));
if (appRoles is null || !appRoles.Any())
{
throw new ArgumentException($"App role not found for \"{servicePrincipal.AppId}\".");
}
var roleClaimsArray = appRoles
.Select(appRole => appRole.Value ?? string.Empty)
.Where(x => !string.IsNullOrEmpty(x))
.ToArray();
return roleClaimsArray is not null && roleClaimsArray.Any()
? roleClaimsArray
: throw new ArgumentException($"App role not found for \"{servicePrincipal.AppId}\".");
}
catch (Exception ex)
{
_logError(_logger, ex);
throw;
}
}
private static IEnumerable<Models.User> ToUserObjects(IEnumerable<Microsoft.Graph.Models.User> graphUsers) =>
graphUsers.Select(graphUser => new Models.User()
{
UserId = graphUser.Id,
DisplayName = graphUser.DisplayName
});
private static Models.User ToUserObject(Microsoft.Graph.Models.User graphUser) =>
new()
{
UserId = graphUser.Id,
DisplayName = graphUser.DisplayName
};
}