Поделиться через


How to use Microsoft Graph and Office 365 API in a Service or in a Windows App/UWP without a graphical interface

Office 365 Graph is just a great way to add Office 365 integration into your application. You’ll find many great information in the Office Dev Center to explain more all what is available. I was interested in adding Office 365 integration in a normal Windows .NET Service on my serveur as well as on a RaspberryPi running Windows IoT Core in a Universal Windows Platform (UWP) application. I want to have a user which can automatically receive emails, do action based on those emails, send calendar items.

The concepts

In both cases, I don’t have any user interface. And it’s an issue as Microsoft Graph/Office 365 API are fully REST and are using OAuth2. And in this case, the mechanism used for authentication require the user to have a user interface to log into. User and passwords are not stored in the client application but an authorization token is sent to the application. The application then have to use this token for authentication. The token is valid for a certain period and need to be refreshed.

You’ll find all the documentation on how OAuth 2.0 is working and how it is implemented with Azure Active Directory here. The graph below explain the full mechanism:

Authorization Code Flow diagram

And this does require the user to enter his credentials on a user interface. When you run a service you just can’t on the service itself. From here, there are 2 ways to solve this issue:

  • Provide your application a special access that will give full right to the entire directory, Office 365 information with some granularity. Granularity is Read, or Read/Write, for mail, calendar…
  • Do the authentication somewhere else and provide the token to the application. This allow much more granularity vs the previous solution and allow to have a normal user with normal privileges connected.

I do not want to go for the first solution because I just want to have one user and just the minimum rights. So I’ve decided to go for the second solution. So far I haven’t found another place where it is document. So my basic idea is to use the following authentication mechanism:

image

Basic idea is to have the user authentication on 1 machine and then pass the authorization token to the other machine. Se let see the solution I used.

For the following steps, I’ll use Microsoft Graph but the process is similar if you’re using Office 365 API or any other API using Azure Active Directory with OAuth 2.0.

Preparing Office 365 tenant and Azure Active Directory

As very well describe in the documentation, you need:

  1. Prepare your Azure Active Directory tenant. Follow the steps here. You will need an Office 365 business account, the rights to add an app into the Azure portal
  2. When following the steps add a “Native client application”
  3. When specifying the App permission, add Microsoft Graph and select what you need
  4. You’ll need to add return URIs. Those are the address that are allow to be redirected on to pass the token/codes after authentication. In the example, I’ll use a Raspberry in a UWP. Name will be “laurellerpitest” and will return on port 81 on page token. So https://laurellerpitest:81/token
  5. Then download the Manifest, save it and open it with Visual Studio for exampleimage
  6. Change “oauth2AllowImplicitFlow” to true and save the file
    image
  7. Upload the saved manifest. This specific modification in the manifest will allow this manipulation of doing the authentication on a machine which is not the one which will use the token. It will just allow to pass the token to a different place in a flow. This is needed only in cases like this one where the authentification has to follow a flow from one place to another. If you are doing the authentication on the same machine you’ll use the token, this is not needed.

You’re now ready and have accomplished all the preparation steps.

Creating the authorization URL

This is where it starts to be interesting. To get the access token which will authorize your app to access the resources, when you read the documentation, you need a code and then send this code to get the access token. But there is a shortcut to directly get the token. You can use an URL like this to get the access token directly:

https://login.microsoftonline.com/12345678-9abc-def1-234567890abcdef12/oauth2/authorize?response_type=token&redirect_uri=http%3a%2f%2flaurellerpitest:81/token&client_id=01234567-89ab-cdef-1234-567890abcdef&resource=https%3A%2F%2Fgraph.microsoft.com%2F

You’ll get the first endpoint in your Azure Portal. It’s the connection endpoint to Microsoft Graph. So you’ll replace this string “12345678-9abc-def1-234567890abcdef12” by your tenant ID. The params:

  • response_type=token: this is where you’re asking for the access token. You may ask for other types of token depending on what you need to do later on. You will find more information on supported token type here. Authentication will happen on a webpage and the token will be returned to the return URI.
  • redirect_uri=https://laurellerpi:81/token (encoded to be compatible with uri parameters): this is the return URI you’ve setup into your portal.
  • client_id=01234567-89ab-cdef-1234-567890abcdef: this is the unique ID of your app that you’ll find as well un the Azure portal.
  • resource=https://graph.microsoft.com/: request access to Microsoft Graph. Can be as well other services suing the same endpoint for which you gave autorisation

If you click on this link, you’ll get an error message on the bottom right of the page:

image

those error messages are really useful to help you find out if you’ve done a mistake somewhere. If you’ve setup everything correctly, you’ll be able to arrive on a page like this:

image

Authentication with user name and password happens on this page.

Depending on the rights you’ve allowed for your app, you may need to have the user agreement to use the app. In this case, it will ask you for your agreement.

And finally, you’re redirected to the redirect URL. And you’ll note that the token is present in the parameter “access_token”.

image

And at this point, there is a “but”. And it’s quite a bit “but”. The parameter is passed following a hash tag (#). This means that the parameter won’t be passed to the server! It is a client only parameter like the other passed as well.

So let see how to get this param passed to the server.

Getting the access token

The return URI look like this with a much longer token than the one represented here:

https://laurellerpitest:81/token#access_token=abc.def.ghi-jkl-mno-pqrstuvwxyz&token_type=Bearer&expires_in=3600&session_state=12345678-1234-1234-1234-1234567890ab

This token is type of Bearer. More info on Bearer token in the RFC. There are multiple type of token but the one used to get access is the Bearer one. You can get other types by requesting other tokens like to refresh your token but you’ll need to have a Bearer on to access the resources. Se at the end of the article, more links and resources to go deeper on this topic.

As it is returned only on the client side, we will need a javascript on the page to gather it and return it to the device. So I’ll need to implement a very simple web server into my UWP app on my RaspberryPi to get this token back.

Here is the full code and I’ll explain it right after. Note: I have produced this code during an internal hackathon. It is a very simple, very striaght forward code. It can of course be done in a much more complete way. But it is demonstrating the core part of the process.

privatereadonlyHttpServer httpServer;

httpServer = newHttpServer(81);

httpServer.StartServer();

    publicsealedpartialclassHttpServer : IDisposable

    {

        privateconstuint BufferSize = 8192;

        privateint port = 80;

        privatereadonlyStreamSocketListener listener;

 

        privatestring token;

 

        publicstring Token

        {

            get

            {

                return token;

            }

 

            internalset

            {

                token = value;

            }

        }

 

       

        public HttpServer(int serverPort)

        {

            listener = new StreamSocketListener();

            port = serverPort;

            listener.ConnectionReceived += (s, e) => ProcessRequestAsync(e.Socket);

        }

 

        publicasyncvoid StartServer()

        {

#pragmawarningdisable CS4014

            await listener.BindServiceNameAsync(port.ToString());

#pragmawarningrestore CS4014

        }

 

        publicvoid StopServer()

        {

            ((IDisposable)this).Dispose();

        }

 

        publicvoid Dispose()

        {

            listener.Dispose();

        }

 

        privateasyncvoid ProcessRequestAsync(StreamSocket socket)

        {

            awaitTask.Run(async () =>

            {

                try

                {

                    // this works for text only

                    StringBuilder request = new StringBuilder();

                    using (IInputStream input = socket.InputStream)

                    {

                        byte[] data = newbyte[BufferSize];

                        IBuffer buffer = data.AsBuffer();

                        uint dataRead = BufferSize;

                        while (dataRead == BufferSize)

                        {

                            await input.ReadAsync(buffer, BufferSize, InputStreamOptions.Partial);

                            request.Append(Encoding.UTF8.GetString(data, 0, data.Length));

                            dataRead = buffer.Length;

                        }

                        Debug.WriteLine("Request received: {0}", request.ToString());

                    }

 

                    using (IOutputStream output = socket.OutputStream)

                    {

                        string requestMethod = request.ToString().Split('\n')[0];

                        string[] requestParts = requestMethod.Split(' ');

 

                        if (requestParts[0] == "GET")

                            await WriteResponseAsync(requestParts[1], output);

                        else

                            return;

                        Debug.WriteLine("Request processed from WebServer");

                    }

                }

                catch (Exception e)

                {

                    Debug.WriteLine("Exception in Socket: {0}", e.ToString());

 

                }

                finally

                {

                    try

                    {

                        socket.Dispose();

                    }

                    catch (Exception e)

                    {

                        Debug.WriteLine("Exception in cleaning exception Socket: {0}", e.ToString());

                    }

                }

 

            });

        }

 

        privateasyncTask WriteResponseAsync(string request, IOutputStream response)

        {

            string pageUtil = "token";

            string strFilePath = request;

            string strResp = "";

            if (strFilePath.Length >= pageUtil.Length)

            {

                if (strFilePath.Substring(1, pageUtil.Length).ToLower() == pageUtil)

                {

                    ProcessResponsePage(response, strFilePath);

                    return;

                }

            }

           

            // send blank text

            await OutPutStream(response, strResp);

        }

 

 

        privateasyncvoid ProcessResponsePage(IOutputStream response, string rawURL)

        {

            string strResp = ""; ;

            if (DecryptProcessResponsePage(rawURL))

                strResp = "OK";

            else

                strResp = BuildScript();

            await OutPutStream(response, strResp);

        }

 

        privatestring BuildScript()

        {

            string strResp = "";

            strResp += "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";

            strResp += "<html xmlns=\"https://www.w3.org/1999/xhtml\"><head><title></title>";

            strResp += "<SCRIPT language=\"JavaScript\">";

            strResp += "if (window.location.href.indexOf(\"#access_token\") > 0)";

            strResp += "{var token = window.location.href.substring(window.location.href.indexOf(\"#access_token=\") + 14);";

            strResp += "token = token.substr(0, token.indexOf(\"&\"));";

            strResp += "var xhr = new XMLHttpRequest(); ";

            strResp += "xhr.open('GET', '/token?access_token=' + token);";

            strResp += "xhr.onreadystatechange = function() {if (xhr.readyState == 4) {boxMSG.innerHTML=xhr.responseText;}}; xhr.send();}";

            strResp += "";

            strResp += "</Script>";

            strResp += "</head><body><span id='boxMSG'></span></body></html>";

            return strResp;

        }

 

        privatebool DecryptProcessResponsePage(string strDecrypt)

        {

            // decode params

            Param[] Params = Param.decryptParam(strDecrypt);

            bool isvalid = false;

            try

            {

                if (Params != null)

                {

                    for (int i = 0; i < Params.Length; i++)

                    {

                        //on cherche le paramètre strMonth

                        int j = Params[i].Name.ToLower().IndexOf("access_token");

                        if (j == 0)

                        {

                            token = Params[i].Value;

                            isvalid = true;

                        }

                    }

                }

 

            }

            catch (Exception)

            {

 

                isvalid = false;

            }

 

 

            return isvalid;

 

        }

 

        privateasyncTask OutPutStream(IOutputStream response, string strResp)

        {

            try

            {

                using (Stream resp = response.AsStreamForWrite())

                {

                    byte[] bodyArray = Encoding.UTF8.GetBytes(strResp);

                    resp.Write(bodyArray, 0, bodyArray.Length);

                    //Flush need to be SYNC. If async, then it goes closed before!

                    //resp.Flush();

                    Debug.WriteLine("OutputStream sent");

                }

            }

            catch (Exception e)

            {

                Debug.WriteLine("Exception in outputstream: {0}", e.ToString());

            }

 

            return;

 

        }

 

       

}

    publicsealedclassParam

    {

        publicstring Name { get; set; }

        publicstring Value { get; set; }

        publicstaticchar ParamStart { get { return'?'; } }

        publicstaticchar ParamEqual { get { return'='; } }

        publicstaticchar ParamSeparator { get { return'&'; } }

        publicstaticParam[] decryptParam(string Parameters, char start = '?')

        {

            Param[] retParams = null;

            int i = Parameters.IndexOf(start);

            String strtocut = Parameters;

            if (i >= 0)

                strtocut = Parameters.Substring(i + 1);

            var strcut = strtocut.Split(ParamSeparator);

            if (strcut != null)

            {

                retParams = newParam[strcut.Length];

                for (int inc = 0; inc < strcut.Length; inc++)

                {

                    var strgood = strcut[inc].Split(ParamEqual);

                    retParams[inc] = newParam();

                    if (strgood.Length == 2)

                    {

                        retParams[inc].Name = strgood[0];

                        retParams[inc].Value = strgood[1];

                    }

                    else

                    {

                        retParams[inc].Name = "";

                        retParams[inc].Value = "";

                    }

 

                }

 

            }

            else {

                retParams = newParam[1];

                retParams[0].Name = "";

                retParams[1].Value = "";

            }

            return retParams;

        }

}

 

The code shows a very simple implementation of an HTTP server supporting only GET. Whenever a request arrives on the specified port, it is processed by the ProcessRequestAsync function. This function get the headers and body of the request and extract the raw URL. Then it calls the WriteResponseAsync to process the request.

The WriteResponseAsync function is just looking at the raw URL from the HTTP request and check if it’s “token”. If it’s token then it calls the function ProcessResponsePage. This function then call the DecryptProcessResponsePage.

The DecryptProcessResponsePage check if there is a param named “access_token” passed in the URL. If yes, then it does extract it and store it and return true. If not, then it returns false. if it’s true, the ProcessResponsePage will return OK and if it’s false, it will return the BuildScript content which create a javascript.

This javascript is very simple as well and minimal, here is how it does looks like once on the client:

if (window.location.href.indexOf("#access_token") > 0)

{

    var token = window.location.href.substring(window.location.href.indexOf("#access_token=") + 14);

    token = token.substr(0, token.indexOf("&"));

    var xhr = new XMLHttpRequest();

    xhr.open('GET', '/token?access_token=' + token);

    xhr.onreadystatechange = function () {

        if (xhr.readyState == 4) { boxMSG.innerHTML = xhr.responseText; }

    };

    xhr.send();

}

 

The script is looking for the #access_token string. And then extract it and pass it to the /token page as a parameter. this call is done thru the XMLHttpRequest class. The result is then put into the boxMSG span. If all goes correctly, then you’ll see an OK on the page.

Here, I’m passing only the access token, not the other elements. But it’s very easy to adapt the code to pass the others. Important one is the expiriaton session to be able to renew it on time. I’m not explaining how to renew the token here but it is explained in the step by step authentication page.

Using the Microsoft Graph REST API

Now, it’s the very easy part. The REST API is simple and straight forward to use.

HttpClient client = new HttpClient();

client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);

HttpResponseMessage response = await client.GetAsync("https://graph.microsoft.com/v1.0/users/");

string retResp =await response.Content.ReadAsStringAsync();

 

You just need to create an HttpClient, then add the Baerer token in the header and call the Graph API you want. This one will get all the users in the Azure Directory. It is returned as a JSON and then you can play with it.

Next steps

As next steps, you can extend the code to ask for refreshed access token.

This is using what is available with the current app model. The app model v2 is much simpler, requires less steps. Some details in these links with the excellent KurveJS framework from Mat Velloso. You have a good comparison of both models here, the protocols and scopes. And I want to thanks Mat as well for his help on this project and putting all this together.

The all up Microsoft Graph API documentation, including the v1.0 and the beta version, is available are here. The API is rich and well get even richer. A good start is https://graph.microsoft.io/. And as authentication, authorization and token access are key point to understand, read as well this article on Microsoft Graph endpoints using converged authentication.

There’s a lot to do and now, you can use it in an embedded device with no UI.

Comments

  • Anonymous
    March 08, 2016
    Hi, Really happy we found this blog post. Great read and addresses a very relevant topic for us. We are two developers working on a app where we want to collect calendar items from a given office 365 account. The app will run permanently and on a regular basis fetch the calendar events. It is important for us that the app can access the events without any user interactions or manual sign ins.In the blog post you mentioned two ways for avoiding a user interface. Since we want our app to run permanently as a background job, we think that the first solution is the best way to go. Do you know how we can provide our app with special access rights to our Office 365 account, or any place we can find such information? Best regards
  • Anonymous
    June 15, 2016
    How do you handle the expiration of the access token? Unless you request a refresh token, the access token will expire after a set time (in your example above, 3600 seconds) and you'd have to get the user to re-grant authorization for another access token.
    • Anonymous
      October 06, 2016
      yes, it does, so you have to refresh it and request a refresh token. It's just an example on how it's working. You can of course use more there using the same principle
  • Anonymous
    December 08, 2016
    HttpClient client = newHttpClient(); should be HttpClient client = new HttpClient(); resolve this mistake in article.
    • Anonymous
      June 20, 2017
      thanks, correcting!
  • Anonymous
    February 02, 2017
    Hi,Thank you for your blog post. Really happy when we found this. We should have an application to create some events on some calendars without webform.But, I've got a question please.I don't try where I associate the authorization URL in my project. I don't try my tenant ID.I've got my ID Application and ID Secret....Thanks for your answer.Regards,
  • Anonymous
    June 26, 2017
    Hello,Thanks for this post.Could you please tell me where can i find the dll for StreamSocketListener class ?Thanks,Best regards,Cao