Billy Cottrell
Billy Cottrell

Reputation: 505

Microsoft Graph getSchedule method results in InvalidAuthenticationToken error code 80049217

The Problem:

I am creating an Angular webapplication where companies can register 1 of their users. With this account they can add their Microsoft resource accounts for later, but other than that they can do nothing until they authorize Microsoft Graph with their Microsoft Account. When they are authorized (and their access token is saved) they can retrieve the booking information of their chosen resource accounts. This information will be shown on different screens throughout the company so they know which meeting rooms are already occupied or booked at a certain moment.

So far I have been able to complete the authorization process, but when trying to retrieve the bookings I received:

  "error": {
    "code": "InvalidAuthenticationToken",
    "message": "CompactToken parsing failed with error code: 80049217",
    "innerError": {
      "request-id": "1c24ceea-fa85-4cd0-bb0d-ab14ae26b65b",
      "date": "2020-02-19T15:54:07"
    }
  }
}

I have no clue about what the problem is exactly. I hope there is someone who is able to help me out. I do want to apologize as this is my first post on StackOverflow and I have no idea whether this is a good post or not and if there is sufficient information.

What I have tried:

Before I started to create the webapplication and api, I have been experimenting with the graph explorer and there I was able to do calendar requests and the response looked promising so I started working on the project.

At first the authorization process I used was the Microsoft authorization process on behalf of the signed-in user. The reason for this is because the user is the one who authorizes and requests booking information. When I was going to start retrieving the booking information I realized that we have to retrieve this information from different resource mailboxes. So because a regular user can't access other users of their organization we would need to have an admin user.

That's when I found about the authorization process we are using right now, namely the Microsoft authorization process without signed-in user. This would require adminconsent and it would be able to retrieve booking information from multiple resource mailboxes.

Now when the authorization process worked completely I wanted to try getting the booking information, but everytime I got the InvalidAuthenticationToken (see above for complete error).

I checked the permissions in the azure portal and changed them in the hope that would solve the issue but apparently not. So now these permissions look like this: Permissions in the azure portal (image)

So I started looking at the authorization process I made. First users authorize through a link on the Angular webapp:

<a 
    href="https://login.microsoftonline.com/common/adminconsent
        ?client_id=**CLIENT_ID**
        &state={{user.email}}
        &redirect_uri=**REDIRECT_URL_TO_API**"
    type="button"
    class="btn btn-icon btn-danger"
    aria-label="error"
>
    <clr-icon shape="error-standard"></clr-icon>
    Authorize
</a>

I have checked the client id which is correct, same thing with the url because it goes to the api without an issue. The state is the user email because this is needed in the loopback api later.

So I login with my microsoft account which has admin privileges and I accepted the permissions that are asked to use. When it finished it redirected to the api I had build.

This api was completely build by using Loopback 3, and it uses MongoDB as database. So to use loopback I had to make 2 models, 1 for the loggedin user (AuthUser) on the website and 1 for the access tokens from Microsoft Graph (MSGraphAuth). Where the AuthUser model has a "hasOne relation" with the MSGraphAuth model.

Aside from the used models I have setup middleware at the /server/boot directory. There I have edited the root.js file so I could add some express routes. The root.js file looks like this:

'use strict';
const express = require('express');
const erouter = require('express').Router();
var cors = require('cors');
const msauth = require('./msAuth.service');
const getbookings = require('./retrieveBookings.service');
module.exports = function(server) {
    // Install a `/` route that returns server status
    var router = server.loopback.Router();
    router.get('/', server.loopback.status());

    var app = express();
    app.use(cors({
        origin: "*"
    }));
    erouter.get('/API/auth', msauth());
    erouter.get('/API/bookings/:userid', getbookings());
    app.use(erouter);
    server.use(app);
    server.use(router);
};

So when Microsoft redirects it will go to the /API/auth route, the code behind looks like this:

'use strict';
const request = require('request');
var querystring = require('querystring');
const server = require("../server");
module.exports = function() {
    return function(req, res, next) {
        var AuthUser = server.models.AuthUser;
        var user = {};
        AuthUser.find({ where: { email: req.query.state } }, function(err, au) {
            if (err) return next(err);
            if (au.length != 1) return res.redirect("https://*WEBSITE*/settings?error=user_not_found");
            user = au[0];
            if (!req.query.admin_consent) return res.redirect("https://*WEBSITE*/settings?error=permission_denied");
            const dataTokenReq = querystring.stringify({
                "client_id": process.env.client_id,
                "scope": "https://graph.microsoft.com/.default",
                "client_secret": process.env.client_secret,
                "grant_type": "client_credentials"
            });
            const postOptions = {
                url: `https://login.microsoftonline.com/${req.query.tenant}/oauth2/v2.0/token`,
                method: 'POST',
                body: dataTokenReq,
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'Host': 'login.microsoftonline.com'
                }
            }
            request(postOptions, function(err, resp, body) {
                if (err) return next(err);
                var body = JSON.parse(body);
                const dataMsAuth = querystring.stringify({
                    "created": new Date().toDateString(),
                    "userId": user.id,
                    "token_type": body.token_type,
                    "expires": body.expires_in,
                    "access_token": body.access_token,
                    "tenant": req.query.tenant
                });
                const postMSAuth = {
                    url: "https://*API_SITE*/api/AuthUsers/" + user.id + "/msauth",
                    method: 'POST',
                    body: dataMsAuth,
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded'
                    }
                }
                request(postMSAuth, function(err, resp, body) {
                    if (err) return next(err);
                    // Update user
                    console.log(body);
                    res.redirect("https://*WEBSITE*/settings?auth=success");
                });
            });
        });
    }
}

So here I first find the user based on the email adress from the state so I could add the tokens to that user, but first I check on issues from the find and admin_consent. When there are no issues I retrieve the token with client id, secret, default scope and grant_type as client_credentials as stated in the documentation. When I received the tokens without issues I post them by accessing the related model of the specified user. (Normally if this is done and there were no issues I would update the user with a boolean so it knows it has been authenticated without accessing the related model, which worked but didn't update in the database for some reason.)

Now I get redirected to the website and I just have to logout to refresh the cookie so It knows that I am authenticated with Microsoft Graph. So when I go to check the bookings of my chosen resource mailboxes I would see this information but this doesn't work.

When I try to retrieve the booking information I ask this through a custom route in my Loopback environment which is /API/bookings/:userid. On this route I have the following code to retrieve the booking information:

'use strict';
const request = require('request');
var querystring = require('querystring');
const server = require("../server");
module.exports = function() {
    return function(req, res, next) {
        /**
         * retrieve bookings user
         */
        var AuthUser = server.models.AuthUser;
        AuthUser.findById(req.params.userid, { include: 'msauth' }, function(err, au) {
            if (err) return next(err);
            if (au == null) return next;
            console.log(au);
            if (au.msauth == null) return next;
            const data = querystring.stringify({
                "schedules": ["[email protected]", *RESOURCE_MAILBOX*],
                "startTime": {
                    "dateTime": "2020-02-20T01:00:00",
                    "timeZone": "Europe/Paris"
                },
                "endTime": {
                    "dateTime": "2020-02-29T23:00:00",
                    "timeZone": "Europe/Paris"
                }
            });
            const postOptions = {
                url: "https://graph.microsoft.com/v1.0/me/calendar/getSchedule",
                method: 'POST',
                body: data,
                headers: {
                    'Authorization': "Bearer " + au.msauth.access_token,
                    'Content-Type': 'application/json',
                    'Prefer': "outlook.timezone='Pacific Standard Time'"
                }
            }
            request(postOptions, function(err, resp, body) {
                if (err) return next(err);
                console.log(body);
                res.json(body);
            })
        });
    }
}

Here I first search for the user that was specified in the route and add the related model with the access token to the resulting user. So with all the information from the search I can retrieve the booking information which for some reason doesn't work.

So if anyone knows what the issue might be pls let me know, or if anyone has tips on the approach that I am taking they are always welcome.

Thanks in advance!

Upvotes: 0

Views: 440

Answers (1)

Billy Cottrell
Billy Cottrell

Reputation: 505

So I have found the solution to what I wanted to achieve, but I had to use the Microsoft library. So I found the Microsoft Graph demo app and edited to what I wanted to achieve. When it worked I just copied it over to my project and edited it further so it uses only what I needed.

Upvotes: 1

Related Questions