news and know-how about microsoft, technology, cloud and more.

Provisioning an Office 365 group with an approval flow and Azure functions-part 2

In part one, we saw how the Microsoft Graph API enables programmatic access to Office 365 groups. Now it's time to let Azure Functions help us with the desired workflow.

For the following steps, an Azure subscription and a Global Admin in the target Office 365 tenant is required.

The plan

We want our provision group function to be able to create a new Office 365 group without any user interaction. So, we need an app with the permission to accomplish the operations in our Office 365 tenant, in the same way as did for the administrator account in part 1. The key is, to create such an application first and to use that access data in our code. The workflow will execute our function, pass the parameters, and the function will do the work. So, these are the necessary steps.

Create a new App

Open the Azure portal (in the target Office 365 tenant) with a Global Admin and click on the the "Azure Active Directory" service. Go to "App registrations". In here, click "New application registration".


...and fill out the new app "provisiongroupfunction" as Web app/API with a fake Sign-on URL as https://localhost:40000 (we won't need that) as follows:


After the app was created, go to "Keys".


Add a new key "clientSecret", make it valid for 1 year and click "Save". Keys can be generated for 1 year, 2 years or without any expiration date. If you chose an expiration (which is usually a good idea), you need to renew the key from time to time.


Now the key is generated. You will not get any access to that key later, so save the key value, best to your OneNote, Notepad or similar tool.


We also need the "Application ID" property: 516b...


And of course, we need to set the permissions: Go to "Required Permissions", select the "Microsoft Graph" and select "Application Permissions" for "Read and write all groups". "Save" the app permissions.


Select more permissions depending on the desired features of the function if needed. We're done with the app, but...

Get the tenant ID

..we need the "Tenant ID" as well. You get that in the "Properties" of the AAD with the "Directory-ID" property as shown here.


The Directory-ID d302... needs to go to our note. Now we're done with the AAD. So we have this data collected:

string tenantId = "d302f5cf-00b3-44af-aff1-2cf91673813d";
string clientId = "516b6d70-05e4-43a7-bab1-6fa2060b04fa";
string clientSecret = "IQBz/fSblC...";

Create the Azure function as a container

The idea is to use server-less technology to provision an Office 365 group. No worries, we won't need Visual Studio, Visual Studio Code (which BTW are great tools and highly recommended for larger code projects...) or any similar environment. We can perform all necessary steps online directly in the Azure Portal.

So, let's create the new Azure function and fill it out... We are working with loose coupled architecture, so we can use any Azure subscription which must not be associated with the Office 365 tenant. This approach allows to build "black boxes" and to tie them together as needed easily. In our case, we let the function run in another Azure subscription.


Of course, Azure functions are a wide topic with many details, but this article concentrates on the solution. To learn more about Azure functions, see here and the videos on channel9.

Use a programing language - PowerShell is good enough

In Azure functions, there are several programing languages supported. We could use C#, F#, Javascript or ... PowerShell. So, let's keep it simple and use PowerShell, so that IT-Admins who are familiar with PowerShell can easily follow and understand this sample. (In my GitHub repository, you find the solution in C# and in PowerShell) , So, we create a new Azure function of type "HttpTrigger - PowerShell" as here.


The function name shall be "ProvisionGroup". Let's create that with the default authorization level "Function" - we don't want to use a user authorization in our scenario.


...and we get the new function with some default code from a template. When clicking "Run", the function shows the basic concept with an HTTP POST operation, a JSON input and a text output as here:


Develop the function and and configure it

The function needs to perform several operations. Keep in mind that Azure functions run in a sandbox, to be more specific, in an App service in a Virtual machine. You can use default functionality, but you need to take care if you need to integrate other libraries or modules that by default are not installed in the Windows environment. We don't have any dependencies in our sample. Currently, PowerShell version 4.0 is available.

Ok, we also need to store the app data somewhere. Instead of having these values in the code, it's more elegant (and safe) to save these as App Settings. In here, we need to add keys for the TenantID. the AppID and the AppSecret as here. The values must be inserted from the app we created above.


Now we're good to code...

The PowerShell code

Get the code from my GitHub repository Officde365scripts at (directly here)  and paste it into the code window.


The functions reads two parameters from the HTTP body: groupname and upn. groupname is the name of the new group and since this is also the email address of the group, this must be compliant. upn is the login name of the group owner who shall be responsible for the group.

Then, it calls the Initialize-Authorization PowerShell function to authenticate with the app values from our AppSettings. If this works, we get an AccessToken that is stored in the global variable $script:APIHeader. This is the key we need to add to each Graph API operation. It must be sent in the HTTP header with the Bearer key name. So, this header is fully generated by the Initialize-Authorization function and can be added for each call.

Now back to the literal functionality. We need to do four operations:

  1. Create the group and get back the GroupID
  2. Get the UserID of the UPN passed as parameter
  3. Add the UserID as owner of the group with the GroupID
  4. Add the UserID as member of the group with the GroupID

Each request is sent as a HTTP POST operation with the PowerShell command $result = Invoke-RestMethod -Method Post and a JSON body as described in part 1. If the HTTP result of the operation is OK or Created, we know that the operation was successful and continue. In all cases, the function itself returns HTTP OK for not stopping the Flow (which will be described in part 3).

Test it

Add the parameters in the "Test" request body textbox.


Now click "Run".


The group will now be created and the user will become owner of the new group. Check the mailbox of the owner. The new group will show up in the groups list as here.


So, if this function works, we get the address of the function.

Get the function endpoint

As last step in this part, we need to save the function URL to use it in the workflow. You get it with the" Get function URL" link as here:


Off to the flow...

After we created and tested the Azure function, we can finish this workflow in part 3.


Comments (14) -

  • Marco Mangiante

    12/27/2017 2:39:53 PM |


    tried to follow the sample but have this error (I omitted sensible data):

    2017-12-27T14:33:26.450 Function started (Id=501fb4e5-03dc-41ee-a1bc-862651be23e4)2017-12-27T14:33:26.575 TenantID [value] -ClientKey [value] -AppID [value] ERROR! The remote server returned an error: (403) Forbidden.2017-12-27T14:33:27.103 Invoke-RestMethod : The remote server returned an error: (403) run.ps1: line 75+ Invoke-RestMethod+ _________________    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebException    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand2017-12-27T14:33:27.135 ERROR! The remote server returned an error: (400) Bad Request.2017-12-27T14:33:27.150 ERROR! The remote server returned an error: (400) Bad Request.2017-12-27T14:33:27.244 Exception while executing function: Functions.GroupEvents. Microsoft.Azure.WebJobs.Script: PowerShell script error. Microsoft.PowerShell.Commands.Utility: The remote server returned an error: (403) Forbidden.2017-12-27T14:33:27.373 Function completed (Failure, Id=501fb4e5-03dc-41ee-a1bc-862651be23e4, Duration=921ms)

    Sincerely, I've tried Azure Functions and have some good result with simple operations like list of groups in my account, but for these request, or even I tried events in groups I have always bad request and forbidden; I even tried to assign all the permissions to the account that I use with the function (and it is also the global account of the tenant where the function is..the request instead are versus another tenant, like I suppose in the sample).

    Could you help me?

  • Marco Mangiante

    12/27/2017 2:57:16 PM |

    If I set tenantID, clientID and secretID that are related to the tenant where there is the function, how to retrieve group information related to another tenant?

  • Toni Pohl

    12/27/2017 3:45:57 PM |

    Hi Marco,
    it seems, your app does not have all required permissions for the operation. Have a look at Graph Explorer and change the permissions for adding a new Office365 group and try this in there. As far as I remember, the app needs to have delegated permissions for reading user mailboxes as well to work properly (will have a look later).
    The API needs to have app permissions for the desired tenant. So, you need to have the app registered per tenant, then use the app credentials in the Azure function. You could make it Multi-Tenant if needed, but you need to handle the apps - f.e. in a database and switch them with an additional paramater to the app, or pass these data to the Azure function from outside (but be aware of security).
    hth! br, Toni

  • Marco Mangiante

    12/27/2017 4:02:03 PM |

    Hi Tony,

    I suppose the issue is related to my approach, i.e., I have created the app in, say, tenant A, and try to access group events in tenant B (the O365 tenant).
    All the permissions that I set are obvioulsy for the account in tenant A, so when I pass to the function the tenantID, clientID and clientSecret they are all realted to tenant A, so I suppose my app doesn't find nothing related to O365.
    So, if I understand what you say, I have to create the app in the O365 tenant and then use Azure Function in the tenant A with all IDs and secret related to the app in tenant B.


  • Marco Mangiante

    12/28/2017 8:29:46 AM |

    Hi Tony,

    I created the app on my O365 tenant and the function on another tenant; to try it I give all the permission and all the delegation on the app; now I can create a group, have a list of groups in O365 tenant, but when I try to retrieve the events of a group with this line of code

    $response = Invoke-RestMethod ''; -Method Get -Headers $script:APIHeader -ContentType 'application/json'

    I have the forbidden error

    2017-12-28T08:25:29.533 Function started (Id=1dfd5cd0-5263-4732-ab38-8cbc3bfeb8f2)2017-12-28T08:25:29.611 TenantID [value] -ClientKey [value] -AppID [value] Invoke-RestMethod : The remote server returned an error: (403) run.ps1: line 46+ Invoke-RestMethod+ _________________    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebException    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand2017-12-28T08:25:30.020 Exception while executing function: Functions.GetGroupEvents. Microsoft.Azure.WebJobs.Script: PowerShell script error. Microsoft.PowerShell.Commands.Utility: The remote server returned an error: (403) Forbidden.2017-12-28T08:25:30.129 Function completed (Failure, Id=1dfd5cd0-5263-4732-ab38-8cbc3bfeb8f2, Duration=592ms)

    If you have any suggestion I opened to any idea Smile

  • Marco Mangiante

    12/28/2017 1:23:25 PM |

    Hi Tony,

    thanks a lot for you kindness. While I'm waiting for help, I found another article and modified your code adding this into Initialize-Authorization function:

    $username = "";
    $password = "pwd_admin_tenant";

    and, always in the function, changed the line

    $body = "grant_type=client_credentials&client_id=$AppID&client_secret=$EncodedKey&resource=$ResourceUrl"


    $body = "grant_type=password&username=$username&password=$password&client_id=$AppID&client_secret=$EncodedKey&resource=$ResourceUrl"

    With these I can see the group calendar events.

    Now I've seen from your addition that maybe I have no permission on AAD, so I follow your new guidelines and let you know.

    Thanks for all: it is fun to work with this new technology and also have had the help that you give to me.

  • Marco Mangiante

    12/28/2017 1:38:18 PM |


    sorry, for the images I see that, for Microsoft Graph, there are 4 items for delegation, but in the image related to the permissions to check they are only 3.

    However, it seems that I have no luck and can't resolve it with your code: I suppose it is my problem; for the purpose of conversation, is it possible that it is related to the fact that my app is on O365 tenant while the function is on another? I ask this because, in the sample that I said previously, if I explicitly insert user and password it works.

  • Stephen Tyson

    1/12/2018 9:04:22 PM |

    Hi Marco,
    I had the same 403 error as you.  One thing that is missing form the steps is after setting the permissions for the application and hitting "Save" you need to hit "Grant Permissions" this will apply the permission you just set for the app. simply hitting save does not apply the granted permissions.  Hope this helps


  • Mike Devonport

    3/16/2018 3:57:02 PM |

    Thank you for this solution example. I tried it and it works like a charm. Very appreciated!

    I might have a simple stupid question: Why are you using the MS Graph instead of just the PNP PowerShell where you have cmdlets like New-PNPUnifiedGroup. Woudn't that be much simpler? Maybe I miss something...

  • Toni Pohl

    3/16/2018 4:36:17 PM |

    Hi Mike,
    thanks! Well, you are right, there are many ways of doing the groups provisioning, PnP PS is absolutely a way to go as well. We did use Graph since it provides the unified endpoint for accessing so much more data out of Office 365 services - just as a showcase and maybe as a door opener for looking into other functions Graph delivers. For us, Graph is the key to Office 365 data and services, we wanted to transport that message with that sample. Are you going to develop other functionality/workflows with similar stories?
    thx, Toni

  • Toni Pohl

    3/16/2018 4:37:48 PM |

    Hi Vincent,
    just saw your msg - thanks for the feedback, will add that information about giving the consent with the "grant permissions" button asap!
    br, Toni

  • Toni Pohl

    3/23/2018 10:26:43 AM |

    Hi Mikael,
    the Graph Beta already can provision Teams. So, it's easy to follow that - pls. check out:
    Just to mention: APIs under the /beta version in Microsoft Graph are in preview and are subject to change. Use of these APIs in production applications is not supported.
    hth, Toni

  • Toni Pohl

    12/21/2018 2:20:04 PM |

    Hi Niklas,
    ok, thanks for your info. Actually, the JSON body should support unicode characters.
    We can forward that to the product group. As far as I have seen, this conversion could help in the meantime: Try replacing the character ä with its UTF8 encoding \u00e4 as stated at and
    hth? br, Toni

Pingbacks and trackbacks (5)+