Skip to content

Lab E6 - Add Entra ID authentication

Table of Contents

In this lab you will add authentication to your API plugin using OAuth 2.0 with Entra ID as the identity provider.

Note

This lab builds on the previous one, Lab E5. You should be able to continue working in the same folder for labs E2-E6, but solution folders have been provided for your reference. The finished solution for this lab is in the /src/extend-m365-copilot/path-e-lab06-add-authentication/trey-research-lab06-END folder.

Note

There are many detailed setup steps for Entra ID in this lab. A new version of Teams Toolkit is available which will automate many of these steps for you; we plan to provide a more streamlined version of the lab shortly.

In this lab you will register Entra ID applications that are used to secure your plugin and API. Before you begin, choose a safe place for your app information. Here are the values you'll need to save:

API Base URL: 
API service Application (client) ID: 
API service Directory (tenant) ID: 
Authorization endpoint: 
Token endpoint: 
API service client secret: 
API scope: 
Plugin service application (client) ID: 
Plugin service client secret: 

Exercise 1: Set up a persistent developer tunnel (optional)

By default, Teams Toolkit creates a new developer tunnel - and thus a new URL for accesing your locally running API - every time you start the project. Normally this is fine because Teams Toolkit automatically updates the URL where needed, but since this lab will be a manual setup, you'll have to manually update the URL in Entra ID and in Teams Developer Portal each time you start the debugger. For that reason, you may wish to set up a persistent developer tunnel with a URL that does not change.

If you don't want to set up a persistent tunnel, open this note ▶▶▶

You are free to skip this exercise and use the developer tunnel provided by Teams Toolkit. Once your project is running, you can copy this URL from the terminal tab 1️⃣ by choosing the "Start local tunnel" terminal 2️⃣; copy the Forwarding URL 3️⃣. Note this URL will change every time you start the project, and you will need to manually update the app registration reply URL (exercise 2 step 1) and the Teams Developer Portal URL (exercise 5 step 1). Developer tunnel URL

Step 1: Install the developer tunnel CLI

Here are the command lines for installing the developer tunnel. Full instructions and download links for the Developer Tunnel are here..

OS Command
Windows winget install Microsoft.devtunnel
Mac OS brew install --cask devtunnel
Linux curl -sL https://aka.ms/DevTunnelCliInstall | bash

Tip

You may have to restart your command line to update the file path before devtunnel commands will work

Once you have it installed, you'll need to log in. You can use your Microsoft 365 account to log in.

devtunnel user login

Be sure to leave the devtunnel command running as you do the exercises in this lab. If you need to restart it, just repeat the last command devtunnel user login.

Step 2: Create and host the tunnel

Then you'll need to set up a persistent tunnel to the Azure Functions local port (7071). You can use these commands and substitute your own name instead of "mytunnel" if you wish.

devtunnel create mytunnel -a --host-header unchanged
devtunnel port create mytunnel -p 7071
devtunnel host mytunnel

The command line will display the connection information, such as:

The devtunnel running in a console window showing the hosting port, the connect via browser URL, and the URL to inspect network activity.

Copy the "Connect via browser" URL and save it as the "API Base URL".

Step 3: Disable the dynamically created tunnel in your project

If your project is running locally, stop it. Then edit .vscode\tasks.json and locate the "Start Teams App task. Comment out the "Start local tunnel" depdendency and add its dependency, "Start Azurite emulator" instead. The resulting task should look like this:

{
    "label": "Start Teams App Locally",
    "dependsOn": [
        "Validate prerequisites",
        //"Start local tunnel",
        "Start Azurite emulator",
        "Create resources",
        "Build project",
        "Start application"
    ],
    "dependsOrder": "sequence"
},

Step 4: Manually override the server URL

Open env/.env.local and change the value of OPENAPI_SERVER_URL to the persistent tunnel URL.

Exercise 2: Register an Entra ID application for your API

Step 1: Add a new Entra ID app registration

Browse to the Entra ID admin center either via the Microsoft 365 Admin center or directly at https://entra.microsoft.com/. Make sure you are logged into your development tenant and not some other.

Once you're there, click "Identity" 1️⃣, then "Applications" 2️⃣, and then "App registrations" 3️⃣. Then click the "+" 4️⃣ to add a new app registration.

The Microsoft Entra admin center showing the list of applications registered and the button to create a 'New regitration'.

Give your application a unique and descriptive name such as "My API Service" 1️⃣. Under "Supported account types", select "Accounts in this organizational directory only (Microsoft only - single tenant) 2️⃣. Under "Redirect URI (optional)" select "Web" and enter the URL of your developer tunnel 3️⃣.

If you didn't make a persistent developer tunnel URL...

...you will have to update the "Redirect URI" field with the new tunnel URL after each time you start your application in Teams Toolkit

Then click "Register" 4️⃣ to register your application.

The app registration page, where you can provide the application name, supported application types, and redirect URI. There is also the 'Register' button to select.

Step 2: Copy application info to a safe place

Copy the Application ID (also called the Client ID) 1️⃣ and the Directory ID (also called the Tenant ID) 2️⃣ to a safe place; you'll need them later. Then click on the Endpoints button 3️⃣ to open the Endpoints flyout.

The overview page of the application registered. There you can copy the Application ID and the Directory ID, as well as you can find the 'Endpoints' command.

Now copy the two endpoint URLs with name "OAuth 2.0 authorization endpoint (v2)" 1️⃣ and "OAuth 2.0 token endpoint (v2)" 2️⃣ and save them in the same safe place.

The panel with the Endpoints of the application. The buttons to copy 'OAuth 2.0 authorization endpoint (v2)' and 'OAuth 2.0 token endpoint (v2)' are highlighted.

Step 3: Create client secret

Next, navigate to "Certificates & secrets" 1️⃣ and click "+ New client secret" 2️⃣. Give your secret a name and choose a duration, then select the Add button. The secret will be displayed; this is your one and only chance to view it as secrets are only displayed in the portal when they're first created. Copy the secret 3️⃣ to your safe storage place.

The 'Certificates & secrets' page from which you can select to create a 'New client secret'.

Step 4: Expose an API Scope

In order to validate calls to your API, you need to expose an API Scope, which represents the permission to call the API. Though these could be very specific - allowing permission to do specific operations via the API - in this case we'll set up a simple scope called "access_as_user".

First, browse to "Expose an API" 1️⃣ and, next to "Application ID URI" click "Add" 2️⃣. A flyout will open on the right; you can stay with the default value which is api://<your application (client) ID>. Go ahead and click "Save and continue" 3️⃣ to proceed.

The 'Expose an API' page of the application registered, with the side panel to set the application unique URI.

Under "Add a scope" enter "access_as_user" as the scope name 1️⃣. Fill in the remaining fields as follows:

Field Value
Who can consent? Admins and users
Admin consent display name Access My API as the user
Admin consent description Allows an API to access My API as a user
User consent display name Access My API as you
User consent description Allows an app to access My API as you
State Enabled

When you're done, click "Add Scope" 2️⃣.

The 'Add a scope' side panel in the 'Expose an API' page of the application registered, with settings for scope name, who can consent the scope, the admin and user display name and description, and the state flag to enable or disable the scope.

Step 5: Save the API Scope

Copy the scope to your safe place as the "API Scope".

The 'Expose an API' page of the application registered, once the custom scope has been created with the button to copy the scope name highlighted.

Exercise 3: Register an Entra ID application for your plugin

Now that you've registered an API for the application, it's time to register the plugin itself.

About two Entra ID app registrations

This lab is a guide on how to adapt assuming you already have a registered application for your API and want to integrate it into the agent as a plugin. That is why we have two app registrations. When creating an API from scratch, you may not always need two app registrations to implement OAuth securely to plug into the agent. Instead, you can use an existing app registration. Here is how you can do it in one single app registration in this learn module

Step 1: Register the plugin

Return to the "App registrations" section and register a second application. This time call it "My API Plugin" 1️⃣, and once again set "Supported account types" to "Accounts in this organizational directory only" 2️⃣.

Under "Redirect URL" select "Web", and this time set it to https://teams.microsoft.com/api/platform/v1.0/oAuthRedirect 3️⃣. This is the Teams location that will handle completed logins to the API Plugin application.

Click the "Register" button 4️⃣ to complete the registration.

The app registration page, where you can provide the application name, supported application types, and redirect URI. There is also the 'Register' button to select.

As before, view the app's "Overview" page and save the Application (client) ID for the API Plugin app.

Step 2: Create a client secret

As before, create a client secret and save it under "Plugin service client secret" in your safe location.

Step 3: Grant permission

Your plugin needs to call your API service, so naturally it needs permission to do that. Begin by navigating to "API permissions". Then click the "APIs my organization uses" tab 1️⃣ and search for your API service 2️⃣. Select your API service from the results 3️⃣.

The 'API permissions' page of the application registered, with the side panel to grant new permissions. The 'APIs my organization uses' tab is selected and the list of applications shows 'My API Service' in the results.

Now you should see your API service application. Select the "access_as_user" permission and click "Add permission".

The side panel to select and add a permission to the application registered. The 'access_as_user' permission is selected and highlighted, together with the 'Add permission' button.

Exercise 4: Update the API app registration with the plugin application ID

Step 1: Add the Plugin app's ID to the API service app

Now the API Service application needs to allow the API Plugin application to issue tokens for it. To enable this, return to the App Registration for your API Service application. Select "Manifest" and find the entry for knownClientApplications 1️⃣. Add your My Plugin App's client ID to this entry as follows:

"knownClientApplications": [
    "<your-plugin-client-id>"
]

Remember to click "Save" 2️⃣ when you're done.

The page to edit the manifest of the application with the 'knownClientApplications' entry and the 'Save' button highlighted.

Exercise 5: Register the OAuth information in the Teams Developer Portal

Now you're apps are all set up, but Microsoft 365 doesn't know anything about it. It wouldn't be safe to store secrets in the app manifest, so Teams has set up a place in the Teams Developer Portal to safely store this information. In this exercise you'll use the Teams Developer Portal to register your OAuth client application so Copilot can authenticate users for it.

Step 1: Create a new OAuth client registration

Browse to the Teams Developer Portal at https://dev.teams.microsoft.com. Select "Tools" 1️⃣ and then "OAuth client registration" 2️⃣.

The UI of the Teams Developer Portal with 'Tools' and 'OAuth client registration' highlighted.

Click "Register client", if you don't have any already registered client application, or "+ New OAuth client registration", if you already have existing client applications, and fill in the form. Several of the fields are from your safely stored information you've been building up over the last few exercises.

Field Value
Name Choose a name you'll remember
Base URL your API service Base URL
Restrict usage by org select "My organization only"
Restrict usage by app select "Any Teams app"
Client ID Your Plugin Application (client) ID
Client secret Your Plugin Application client secret
Authorization endpoint Your authorization endpoint (same for both API Service and API Plugin apps)
Token endpoint Your token endpoint (same for both API Service and API Plugin apps)
Refresh endpoint Your token endpoint (same for both API Service and API Plugin apps)
API scope Your API Service application's scope

the page to register a new OAuth client in the Teams Developer Portal. There is a list of fields to configure the client registration settings.

If you didn't make a persistent developer tunnel URL...

...you will have to update the "Base URL" field with your new tunnel URL each time you start your application in Teams Toolkit

Step 2: Save your OAuth registration ID

The result of registering an OAuth client in the Teams Developer Portal. There is a box confirming the registration and providing a 'Registration ID' for reference.

The portal will display your OAuth client registration ID. Save this for the next step.

Exercise 6: Update your application package

Step 1: Update the Plugin file

Open your working folder in Visual Studio Code. In the appPackage folder, open the trey-plugin.json file. This is where information is stored that Copilot needs, but is not already in the Open API Specification (OAS) file.

Under Runtimes you will find an auth property with type of "None", indicating the API is currently not authenticated. Change it as follows to tell Copilot to authenticate using the OAuth settings you saved in the vault.

"auth": {
  "type": "OAuthPluginVault",
  "reference_id":  "${{OAUTH_CLIENT_REGISTRATION_ID}}"
},

Then add this line to your env/.env.local file:

OAUTH_CLIENT_REGISTRATION_ID=<registration id you saved in the previous exercise>

The next time you start and prompt your API plugin, it should prompt you to sign in. However we've done nothing to secure the application; anyone on the Internet can call it! In the next step you'll update the application code to check for a valid login and access the API as the actual Microsoft 365 user instead of "Avery Howard" (which is a name from Microsoft's fictitious name generator).

Exercise 7: Update the application code

Step 1: Install the JWT validation library

From a command line in your working directory, type:

npm i jwt-validate

This will install a library for validating the incoming Entra ID authorization token.

Warning

Microsoft does not provide a supported library for validating Entra ID tokens in NodeJS, but instead provides this detailed documentation on how to write your own. Another useful article is also available from Microsoft MVP Andrew Connell.

This lab uses a community provided library written by Waldek Mastykarz, which is intended to follow this guidance. Note that this library is not supported by Microsoft and is under an MIT License, so use it at your own risk.

If you want to track progress on a supported library, please follow this Github issue.

Step 2: Add environment variables for your API

In the env folder in your working directory, open env.local and add these lines for your API Service app's client ID and tenant ID.

API_APPLICATION_ID=<your-api-service-client-id>
API_TENANT_ID=<your-tenant-id>

To make these values available inside your code running in Teams Toolkit, you also need to update the teamsapp.local.yml file in the root of your working folder. Look for the comment "Generate runtime environment variables" and add the new values under the STORAGE_ACCOUNT_CONNECTION_STRING:

  - uses: file/createOrUpdateEnvironmentFile
    with:
      target: ./.localConfigs
      envs:
        STORAGE_ACCOUNT_CONNECTION_STRING: ${{SECRET_STORAGE_ACCOUNT_CONNECTION_STRING}},
        API_APPLICATION_ID: ${{API_APPLICATION_ID}}
        API_TENANT_ID: ${{API_TENANT_ID}}

Step 3: Update the identity service

At this point, OAuth login should work and provide a valid access token, but the solution isn't secure unless the code checks to make sure the token is valid. In this step, you'll add code to validate the is token and extract information such as the user's name and ID.

In the src/services folder, open IdentityService.ts. At the top of the file along with the other import statements, add this one:

import { TokenValidator, ValidateTokenOptions, getEntraJwksUri } from 'jwt-validate';

Then, right under the class Identity statement, add this line:

    private validator: TokenValidator;

Now look for the comment

// ** INSERT REQUEST VALIDATION HERE (see Lab E6) **

Replace the comment with this code:

// Try to validate the token and get user's basic information
try {
    const { API_APPLICATION_ID, API_TENANT_ID } = process.env;
    const token = req.headers.get("Authorization")?.split(" ")[1];
    if (!token) {
        throw new HttpError(401, "Authorization token not found");
    }

    // create a new token validator for the Microsoft Entra common tenant
    if (!this.validator) {
        // We need a new validator object which we will continue to use on subsequent
        // requests so it can cache the Entra ID signing keys
        // For multitenant, use:
        // const entraJwksUri = await getEntraJwksUri();
        const entraJwksUri = await getEntraJwksUri(API_TENANT_ID);
        this.validator = new TokenValidator({
            jwksUri: entraJwksUri
        });
        console.log ("Token validator created");
    }

    // Use these options for single-tenant applications
    const options: ValidateTokenOptions = {
        audience: `api://${API_APPLICATION_ID}`,
        issuer: `https://sts.windows.net/${API_TENANT_ID}/`,
        // NOTE: If this is a multi-tenant app, look for 
        // issuer: "https://sts.windows.net/common/",
        // Also you may wish to manage a list of allowed tenants
        // and test them as well
        //   allowedTenants: [process.env["AAD_APP_TENANT_ID"]],
        scp: ["access_as_user"]
    };

    // validate the token
    const validToken = await this.validator.validateToken(token, options);

    userId = validToken.oid;
    userName = validToken.name;
    userEmail = validToken.upn;
    console.log(`Request ${this.requestNumber++}: Token is valid for user ${userName} (${userId})`);
}
catch (ex) {
    // Token is missing or invalid - return a 401 error
    console.error(ex);
    throw new HttpError(401, "Unauthorized");
}

Learn from the code

Have a look at the new source code. First, it obtains the token from the Authorization header in the HTTPs request. This header contains the word "Bearer", a space, and then the token, so a JavaScript split(" ") is used to obtain only the token.

Also note that the code will throw an exception if authentication should fail for any reason; the Azure function will then return the appropriate error.

The code then creates a validator for use with the jwks-validate library. This call reads the latest private keys from Entra ID, so it is an async call that may take some time to run.

Next, the code sets up a ValidateTokenOptions object. Based on this object, in addition to validating that the token was signed with Entra ID's private key, the library will validate that:

  • the audience must be the same as the API service app URI; this ensures that the token is intended for our web service and no other

  • the issuer must be from the security token service for our tenant

  • the scope must match the scope defined in our app registration, which is "access_as_user".

If the token is valid, the library returns an object with all the "claims" that were inside, including the user's unique ID, name, and email. We will use these values instead of relying on the fictitious "Avery Howard".

If your app will be multi-tenant

Check the comments in the above code for notes about validating tokens for a multi-tenant app

Once the code has a userId it will look for a Consultant record for the user. This was hard-coded to Avery Howard's ID in the original code. Now it will use the user ID for the logged in user, and create a new Consultant record if it doesn't find one in the database.

As a result, when you run the app for the first time, it should create a new Consultant for your logged-in user with a default set of skills, roles, etc. If you want to change them to make your own demo, you can do that using the Azure Storage Explorer

The Azure Storage Explorer in action while editing the Consultant table. The actual current user is highlighted.

Note that project assignments are stored in the Assignment table and reference the project ID and the assigned consultant's consultant ID.

Exercise 8: Test the application

Before you test the application, update the manifest version of your app package in the appPackage\manifest.json file, follow these steps:

  1. Open the manifest.json file located in the appPackage folder of your project.

  2. Locate the version field in the JSON file. It should look something like this:
    json "version": "1.0.0"

  3. Increment the version number to a small increment. For example, change it to:
    json "version": "1.0.1"

  4. Save the file after making the change.

Step 1: (Re)start the application

If your app is still running from an earlier lab, stop it to force it to re-create the application package.

Then press F5 to run the application again, and install it as before.

Prompt the plugin, "What Trey projects am I assigned to?". You may see a confirmation card asking if it's OK to call your API. No authentication is happening here; click "Allow Once" to proceed.

Microsoft 365 Copilot showing a confirmation card asking if it is ok to call your API. There are buttons to 'Always allow', 'Allow once', or 'Cancel.'

The confirmation card will be replaced with a login card. Click "Sign in to Trey" to sign in. At first you should see a pop-up window asking you to log in and to consent to permissions. On subsequent vists this may be hidden as your credentials have been cached by Entra ID in your local browser.

Microsoft 365 Copilot showing a login card with a button to 'Sign in to Trey' and another one to 'Cancel.'

There are cases where your admin has not allowed you to consent as a user and may see something like below: The Microsoft Entra popup dialog asking for an admin approval to consume the API.

This is because the admin has restricted applications to allow user consent tenant wide. In this case, you have to request admin to manually grant global consent for all users for the plugin API registration as below:

The 'API permissions' page of the 'API Plugin' application registered in Microsoft Entra with the 'Grant admin consent ...' command highlighted.

The login card should be replaced by Copilot's response to your prompt. Since you were just added to the database, you aren't assigned to any projects.

Since you were just added to the database, you're not assigned to any projects.

The response from the 'Trey Genie' agent when the actual user doesn't have any assigned project.

Ask Copilot to add you to the Woodgrove project. Copilot will press you for details if you forgot to include any required values.

The response from the 'Trey Genie' agent when adding the current user to a project. If some information are missing, Copilot asks to provide them. Once all the information are provided, the agent provides a confirmation of the action.

Now check out your default skills and confirm the project assignment by asking, "What are my skills and what projects am I assigned to?"

CONGRATULATIONS!

You have completed lab E6, Add Entra ID authentication!

Want to try something cool? How about adding a Graph Connector to your solution?