0

I want to obtain a list of my shifts from the Microsoft Teams 'Shifts' App using NodeJS.

Shifts App image

I believe the Microsoft Teams Shifts app comes as standard with Microsoft Teams.

The shifts I want to obtain are for a specific 'Schedule' (although there is only one available at the moment).
As an example, lets use the shift name of "Main Shift".

The filter settings I use to view the current shifts in Microsoft Teams are as follows:

Microsoft Teams Shifts View Settings

The shifts look like this when filtered:

List of shifts

I registered a new Azure AD App in my portal.azure.com.
This app is called microsoft-teams-shifts and has a secret called microsoft-teams-shifts-secret.

This application has the relevant 'Microsoft Graph API/ Permissions': My graph permissions

I have followed the documentation on the Microsoft website for authenticating and communicating with the Microsoft Graph API: https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-nodejs-console

But I every time I try to use the access token I am given, I keep receiving a 403 - Unauthorised error, stating:

{
  error: {
    code: 'Forbidden',
    message: "Missing role permissions on the request. API requires one of 'Schedule.Read.All, Schedule.ReadWrite.All'. Roles on the request ''.",
    innerError: {
      date: '2023-04-14T08:00:04',
      'request-id': 'xxxx',
      'client-request-id': 'xxxx'
    }
  }
}

My authentication code looks like this (hardcoded values for now while I test):

// auth.js

const msal = require('@azure/msal-node');
// Taken from:
// portal.azure.com -> AD -> App Registrations -> 'microsoft-teams-shifts' -> Application (client ID)
const CLIENT_ID = 'xxxx'; 

// Taken from:
// 'microsoft-teams-shifts' -> Certificates & secrets -> 'microsoft-teams-shifts-secret' (the actual secret value, not the ID of it)
const CLIENT_SECRET = 'xxxx';

// Taken from:
// portal.azure.com -> AD -> App Registrations -> 'microsoft-teams-shifts' -> Directory (tenant) ID
const TENANT_ID = 'xxxxx';

const config = {
  auth: {
    clientId: CLIENT_ID,
    clientSecret: CLIENT_SECRET,
    authority: `https://login.microsoftonline.com/${TENANT_ID}`,
  },
};

const cca = new msal.ConfidentialClientApplication(config);

// Function to acquire a token
async function getToken() {
  const tokenRequest = {
    scopes: ['https://graph.microsoft.com/.default'], // Include the required scope(s) here
  };

  try {
    const authResult = await cca.acquireTokenByClientCredential(tokenRequest);
    return authResult.accessToken;
  } catch (error) {
    console.log(error);
  }
}

module.exports = getToken;

And I use this token to make a request to the graph API:

const axios = require('axios');
const getToken = require('./auth'); // Import the getToken function from auth.js

async function fetchData() {
  try {
    const accessToken = await getToken(); // Call the getToken function to get the access token

    // Make the API call with Axios and pass the access token and required request headers
    // The Team ID is my Microsoft Teams 'team' ID
    const response = await axios.get('https://graph.microsoft.com/v1.0/teams/myTeamIDHere/schedule', {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
        'roles': 'Schedule.Read.All', // Include the required role permission
      },
    });

    // Process the response
    const data = response.data;
    console.log(data);
  } catch (error) {
    console.log(error.response.data.error);
  }
}

fetchData();

This exact request works perfectly fine via the Graph Explorer, with the same URL endpoint.

When I compare the auth token I get from my NodeJS code with the one I can see in the Graph Explorer, I can see that the:

  • aid figures are different (but that kind of makes sense) The aid in my access token corresponds with the CLIENT_ID I specified in my auth code. The one given to me by Graph Explorer has a different one presumably because it's an app of it's own on MS's side.
  • tid is exactly the same in both JWTs
  • The wids array is different though, there is a different value in the one Microsoft Graph generates. Are these supposed to match and that's why my code doesn't work?

Can anyone spot where I am going wrong with this?

nopassport1
  • 1,821
  • 1
  • 25
  • 53

1 Answers1

1

Note that, you need to add Application permissions while using client credentials flow to get access token. Your query runs in Graph Explorer as it works with Delegated permissions where user will be signed in.

I tried to reproduce the same in my environment via Postman and got below results:

I registered one Azure AD application and granted same API permissions as below:

enter image description here

Now, I generated access token using client credentials flow via Postman with below parameters:

POST https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token
grant_type:client_credentials
client_id: <appID>
client_secret: <secret> 
scope: https://graph.microsoft.com/.default

Response: enter image description here

When I used above token to call below Graph query, I got same error as you like below:

GET https://graph.microsoft.com/v1.0/teams/<TeamID>/schedule
Authorization: Bearer <paste_token>

Response:

enter image description here

To resolve the error, you need to add Schedule.Read.All permission of Application type like below:

enter image description here

When I generated the token again and used it to call below Graph query, I got response successfully like below:

GET https://graph.microsoft.com/v1.0/teams/<teamid>/schedule

MS-APP-ACTS-AS: <userID>
Authorization: Bearer <paste_token>

Response:

enter image description here

In your case, make sure to grant Schedule.Read.All permission of Application type in your application and add MS-APP-ACTS-AS: <userID> header while calling Graph query.

You can check roles claim in decoded token to check permissions it has like below:

enter image description here

Sridevi
  • 10,599
  • 1
  • 4
  • 17
  • Thanks. Where is this `MS-APP-ACTS-AS` coming from? How can I declare it? – nopassport1 Apr 14 '23 at 10:14
  • In your code to make graph request, replace `'roles': 'Schedule.Read.All',` with `MS-APP-ACTS-AS: ,` inside headers section. – Sridevi Apr 14 '23 at 10:17
  • What is the ``? Also, is this the right thing to do? If I understand correctly, this means that the NodeJS application will be sending a request to the Graph API on behalf of a user - is that right? I'm not sure if that's what I wanted to achieve. I wanted to fetch the shifts so that I can compare this data with another source in my NodeJS app. Is this the only way to fetch the shifts via Graph API (to send requests on behalf of users)? – nopassport1 Apr 14 '23 at 10:23
  • 1
    When I ran the request without including `MS-APP-ACTS-AS` header, I got error like [this](https://i.imgur.com/IM6YgIz.png). To resolve it, I included the header with user ID that send requests on behalf of that user. Alternatively, you need to use interactive flows like authorization code flow, username password flow etc... that works with **Delegated** permissions to generate token where user sign-in is required. AFAIK, those are the only ways to fetch the shifts via Graph API. – Sridevi Apr 14 '23 at 10:34