diff --git a/SharepointToolbox/Services/GraphUserSearchService.cs b/SharepointToolbox/Services/GraphUserSearchService.cs
new file mode 100644
index 0000000..3af13ec
--- /dev/null
+++ b/SharepointToolbox/Services/GraphUserSearchService.cs
@@ -0,0 +1,53 @@
+using SharepointToolbox.Infrastructure.Auth;
+
+namespace SharepointToolbox.Services;
+
+///
+/// Searches tenant users via Microsoft Graph API.
+/// Used by the people-picker autocomplete in the User Access Audit tab.
+///
+public class GraphUserSearchService : IGraphUserSearchService
+{
+ private readonly GraphClientFactory _graphClientFactory;
+
+ public GraphUserSearchService(GraphClientFactory graphClientFactory)
+ {
+ _graphClientFactory = graphClientFactory;
+ }
+
+ public async Task> SearchUsersAsync(
+ string clientId,
+ string query,
+ int maxResults = 10,
+ CancellationToken ct = default)
+ {
+ if (string.IsNullOrWhiteSpace(query) || query.Length < 2)
+ return Array.Empty();
+
+ var graphClient = await _graphClientFactory.CreateClientAsync(clientId, ct);
+
+ // Use $filter with startsWith on displayName and mail.
+ // Graph API requires ConsistencyLevel=eventual for advanced queries.
+ var escapedQuery = query.Replace("'", "''");
+ var response = await graphClient.Users.GetAsync(config =>
+ {
+ config.QueryParameters.Filter =
+ $"startsWith(displayName,'{escapedQuery}') or startsWith(mail,'{escapedQuery}') or startsWith(userPrincipalName,'{escapedQuery}')";
+ config.QueryParameters.Select = new[] { "displayName", "userPrincipalName", "mail" };
+ config.QueryParameters.Top = maxResults;
+ config.QueryParameters.Orderby = new[] { "displayName" };
+ config.Headers.Add("ConsistencyLevel", "eventual");
+ config.QueryParameters.Count = true;
+ }, ct);
+
+ if (response?.Value is null)
+ return Array.Empty();
+
+ return response.Value
+ .Select(u => new GraphUserResult(
+ DisplayName: u.DisplayName ?? u.UserPrincipalName ?? "Unknown",
+ UserPrincipalName: u.UserPrincipalName ?? string.Empty,
+ Mail: u.Mail))
+ .ToList();
+ }
+}