Send Azure AD Guest User Invitations via Graph API

The built in controls in Azure AD for Guest User invitations are great for most cases. You can lock down guest invitations to specific users or groups and even specific recipient domains. When we look at more highly secure tenancies however, we often see requirements for approval flows or custom workflows to be associated with Guest User invitations.

This isn’t something available in the native GUIs but, as usual, that doesn’t mean it’s not possible! This entire process can be automated and built into a simple Graph API call. We can then expand out what we build into a Logic App/Power Automate flow. Using the different Microsoft technologies, the entire process can be customized to an endless degree. Even non-Microsoft technologies such as Service Management tools can kick off the process and integrate nicely into Azure Automation etc.

To help people (including myself on several projects) to build out their custom processes around guest user invitations, I’ve built the below PowerShell function to provision a guest user invitation via Graph API

In this post, we’ll walk through setting up the environment for the function and how to run it.

Setting up an app registration

The first thing we need for our Graph call is an application registration. This can be set up in Azure AD and assigned the permissions as below:

Configure the new app registration with a name. A redirect URL of http://localhost can be set as we don’t need redirect.

Under API Permissions, add and grant User.Invite.All, User.ReadWrite.All and Directory.ReadWrite.All permissions. Depending on how you are connecting this can be delegated or application permissions but as I need to automate the calls, I’ve selected application permissions. Ensure to grant consent on the permissions after adding them

Finally, create a Client Secret and take note of it for use in the Graph Connection

Parameters Required

Before we can run the function, we need to collect some information. We need to gather the below information about our tenant:

  • The Client ID of the application registration we created above
  • The Tenant ID (Directory ID) from Azure AD
  • The Client Secret we noted when we created the application registration
  • A URL to redirect users to after completion, this isn’t mandatory and the default is https://myapps.microsoft.com

Once we have all this information, we can import the function by running:

import-module <Filepath to PS1 file>\graph-Send-GuestInvitation.ps1

Once imported, we can run our function with the below command:

SendGuestInvitation -UserEmail <Guest user email> -ClientSecret <Your Client Secret> -ClientID <Your Client ID> -TenantID <Your Tenant ID>

When the function is run, it will return an invitation object. This object contains the details of the invitation as well as the “inviteRedeemURL” which can be sent to the guest to redeem their invitation in a more custom, branded email from an offical corporate email address.

This function can be found on GitHub here

Note, this code is for illustration purposes and not fit for running in production. Please ensure you understand any code you are running in a production environment.

function GetGraphToken {
    # Azure AD OAuth Application Token for Graph API
    # Get OAuth token for a AAD Application (returned as $token)
    <#
    .SYNOPSIS
    This function gets and returns a Graph Token using the provided details
    

    .PARAMETER clientSecret
    -is the app registration client secret

    .PARAMETER clientID
    -is the app clientID

    .PARAMETER tenantID
    -is the directory ID of the tenancy
    
    #>
    Param(
        [parameter(Mandatory = $true)]
        [String]
        $ClientSecret,
        [parameter(Mandatory = $true)]
        [String]
        $ClientID,
        [parameter(Mandatory = $true)]
        [String]
        $TenantID

    )

    
    
    # Construct URI
    $uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
     
    # Construct Body
    $body = @{
        client_id     = $clientId
        scope         = "https://graph.microsoft.com/.default"
        client_secret = $clientSecret
        grant_type    = "client_credentials"
    }
     
    # Get OAuth 2.0 Token
    $tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
     
    # Access Token
    $token = ($tokenRequest.Content | ConvertFrom-Json).access_token
    return $token
}

Function SendGuestInvitation {
    <#
    .SYNOPSIS
    This function gets Graph Token from the GetGraphToken Function and uses it to request a new guest user
    
    .PARAMETER UserEmail
    -UserEmail is the email address of the requested user
    
    .PARAMETER clientSecret
    -is the app registration client secret

    .PARAMETER clientID
    -is the app clientID

    .PARAMETER tenantID
    -is the directory ID of the tenancy
    
    .PARAMETER tenantID
    -A URL to redrect to after the invitation is redeemed
    #>
    Param(
        [parameter(Mandatory = $true)]
        [String]
        $UserEmail,
        [parameter(Mandatory = $true)]
        [String]
        $ClientSecret,
        [parameter(Mandatory = $true)]
        [String]
        $ClientID,
        [parameter(Mandatory = $true)]
        [String]
        $TenantID,
        [parameter(Mandatory = $false)]
        [String]
        $RedirectURL = "https://myapps.microsoft.com"

    )

    $token = GetGraphToken -ClientSecret $ClientSecret -ClientID $ClientID -TenantID $TenantID

    $apiUri = 'https://graph.microsoft.com/beta/invitations/'
    $body = "{'invitedUserEmailAddress': '$UserEmail','inviteRedirectUrl': '$RedirectURL'}"
    
    $invitation = (Invoke-RestMethod -Headers @{Authorization = "Bearer $($token)" } -Uri $apiUri -Method Post -ContentType 'application/json' -Body $body)

    Return $invitation
}

Graph API & PowerShell: Function To Use A Refresh Token To Renew An Access Token

In the process of updating my library of standard scripts and functions, I’ve taken the opportunity to change a lot of them to use Graph API rather than legacy EWS or PowerShell Modules. Using Graph helps to make them much more efficient and flexible and a lot of the functionality that I need is there anyway.

One challenge I came across with Graph was when running with delegated permissions and avoiding authenticating constantly for unattended scripts. When running as an app we can pipe in our App Registration details and away we go but when you bring delegated permissions and MFA into the loop, that becomes a challenge.

I’ve put together the below function (located here on GitHub) to allow me to use a refresh token and renew my access token to keep access alive. When using access tokens, they will by default last for an hour, needing to be renewed after that. Refresh tokens (which last 14 days) can then be used to renew this access token and get a new refresh token in the process.

The function itself takes in the following values:

-Token: The existing refresh token

-tenantID: The ID of your tenant (tenant.onmicrosoft.com)

-ClientID: Client ID of your App Reg

-Secret: The secret of your app reg

-Scope: A comma delimited list of your access scope

It will then pass back a new token object complete with renewed access and refresh tokens. I generally store these tokens in Azure Key Vault and update them at refresh time. This means my apps can refresh tokens when they need to and save them securely, only accessing at run time.

function RefreshAccessToken{

<#

.SYNOPSIS

Refreshes an access token based on refresh token



.RETURNS

Returns a refreshed access token



.PARAMETER Token

-Token is the existing refresh token



.PARAMETER tenantID

-This is the tenant ID eg. domain.onmicrosoft.com



.PARAMETER ClientID

-This is the app reg client ID



.PARAMETER Secret

-This is the client secret



.PARAMETER Scope

-A comma delimited list of access scope, default is: "Group.ReadWrite.All,User.ReadWrite.All"



#>

Param(

[parameter(Mandatory = $true)]

[String]

$Token,

[parameter(Mandatory = $true)]

[String]

$tenantID,

[parameter(Mandatory = $true)]

[String]

$ClientID,

[parameter(Mandatory = $false)]

[String]

$Scope = "Group.ReadWrite.All,User.ReadWrite.All",

[parameter(Mandatory = $true)]

[String]

$Secret

)



$ScopeFixup = $Scope.replace(',','%20')

$apiUri = "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token"

$body = "client_id=$ClientID&scope=$ScopeFixup&refresh_token=$Token&redirect_uri=http%3A%2F%2Flocalhost%2F&grant_type=refresh_token&client_secret=$Secret"

write-verbose $body -Verbose

$Refreshedtoken = (Invoke-RestMethod -Uri $apiUri -Method Post -ContentType 'application/x-www-form-urlencoded' -body $body )



return $Refreshedtoken



}