Is it not guaranteed when the graph service call returns that the user has been added to the tenant?

Viewed 448

My app has a user management interface for delegated administration by users without sufficient permissions to our Azure tenant. When a new user is added to the app, we check if the user already exists in the tenant and, if not, we invite them. Something like this:

var existingUser = // Try to find the user in the graph by email
if (existingUser == null)
{
    // User doesn't exist, add and get them.
    var invitation = new Invitation
    {
        SendInvitationMessage = false,
        InvitedUserEmailAddress = user.Email,
        InviteRedirectUrl = _inviteSettings.AppUrl
    };

    await _graphClient
        .Invitations
        .Request()
        .AddAsync(invitation);

    existingUser = // Get the user from the graph by email...
}

How long should it take for the user to be available through a graph call?

I've seen it unofficially in a comment here that it could take 5 or 10 seconds. (using PowerShell so maybe unrelated?) The MS docs says:

When the user is invited, a user entity (of userType Guest) is created and can now be used to control access to resources.

Further, the response example shows the necessary properties returned from the request.

The problem though is at runtime the users are not always immediately found. Sometimes it takes two or three tries to get it to complete successfully and the time to do so has exceeded the 10 seconds referenced above.

Is it not guaranteed when the graph service call returns that the user has been added to the tenant?


Note: I updated the title from "How long should it take for the user to be available through a graph call?" as I'm more concerned about a guarantee than a typical time span.


2 Answers

It does not appear there's any guarantee a newly invited, external user will be available through all graph endpoints/calls despite the guest user having been created along with the invitation. That user already has its email and some other details populated but still cannot be found with:

// Filter by email to avoid calculating the ugly
// user principal names for external users.
await _graphClient
    .Users
    .Request()
    .Filter($"mail eq '{email}'")
    .GetAsync();

That apparently works fine for domain users and guests who've been in the tenant "long enough" (whatever that means) but not those just invited. Luckily, there's another way.

The Invite returned from the graph call includes a read-only copy of the guest user. You can take the InvitedUser.Id prop value and make a call against the Users endpoint which apparently makes the user available immediately:

await _graphClient
    .Users[invite.InvitedUser.Id]
    .Request()
    .GetAsync();

Additional notes: fixing this exposed another issue with assigning the external user an app role. Trying to assign them the role ASAP always resulted in a ServiceException of "Not a valid reference update" using:

await _graphClient
    .ServicePrincipals[principalId]
    .AppRoleAssignedTo
    .Request()
    .AddAsync(assignment);

That is the recommended way to do it per Microsoft so I tried to stick with it and added some crude retry logic to see what I see (use at your own risk and peril):

private async Task<R> RetryTask<R>(Func<Task<R>> func, int max, int delay = 1000, int n = 0)
{
    try
    {
        if (max - n == 1) return default(R);

        await Task.Delay(n * delay);

        return await func();
    }
    catch (Exception e)
    {
        return await RetryTask(func, max, delay, n + 1);
    }
}

The problem with that is it wouldn't complete the assignment after waiting 10 seconds; way too long from a usability standpoint. So, I tried it the way that's not recommended and yahtzee!. I was unable to find the reasoning behind the recommendation so I can't speak to the risks doing it against the Users and not ServicePrincipals.

await _graphClient
    .Users[userId]
    .AppRoleAssignments
    .Request()
    .AddAsync(assignment);

I test it in my side with graph api to invite a guest user and once the api request completed, the user is available in AD (almost no waiting time). Then after some test, I think your problem is not related to the time for a guest user available in AD, the problem was caused by asynchronous of your code running.

The code await _graphClient.Invitations.Request().AddAsync(invitation); need to take a few seconds to complete the invite guest operation. But the next line of code existingUser = will be executed immediately even though the previous code hasn't completed. So it will response the user can't be found.

The bad thing is I can't find any properties of the request await _graphClient.Invitations.Request().AddAsync(invitation); which can help us to know if the request is completed. So we can just use a solution which is not so good, the code should be like this:

await graphClient.Invitations.Request().AddAsync(invitation);

User user = null;

while (user == null)
{
    Thread.Sleep(1000);
    try
    {
        user = await graphClient.Users["email"].Request().GetAsync();
    }catch (Exception e)
    {
        user = null;
    }
}

Console.WriteLine(user.DisplayName);

By the way, to avoid infinite loops of the while, you can also add a variable count. Each time add 1 to count, when count > 60, exist the while loop.

Related