Post

Azure EntraID in Single Page Application published on Teams for organization

In this article, we will explore the the vast field of Microsoft Entra identity platform, with highlighting key concepts such as Entra ID, Microsoft Authentication library(MSAL), multi-tenant App Registrations, Service Principal, Graph API, and exposing the application in organization’s Teams.

To understand the complete development process of an application that leverages organizational resources using Entra ID and Graph API, and ultimately gets published on the organization’s Teams, I’ve created a simple project with minimum implementation. The application’s features include displaying the logged-in user’s profile picture and presence, fetched by Graph API, after signing in with a Microsoft account email.

The application is registered on the Microsoft identity platform to be exposed as an Enterprise Application to my own tenant, and with that, the application will be published in my organization’s Teams.

To keep the cost of implementation and deployment at lowest, the application is deployed on GitHub Pages as Single Page Application, and yes, it’s a static web application, so naturally its features are limited. But it serves to demonstrate the development process of an application that sits on Microsoft identity platform, so let’s get started.

0. Deployment Architecture

Codes of the application are at: GitHub Repository

C4Deployment
    title  
    
    Deployment_Node(teams, "Teams") {
        Container(Teams, "Teams", "", "")
    }

    Deployment_Node(github, "GitHub SPA host") {
        Container(React, "React with MSAL", "Single Page Application", "")
    }

    Deployment_Node(azure, "Azure / Entra ID") {
        Deployment_Node(organizationalResources, "Publishing Organization") {
            ContainerDb(Organizational_Resources, "Organizational Resources", "")
            
            Deployment_Node(publishingOrganization, "Local representation of registration") {
                Container(Service_Principal, "Service Principal", "", "")
                Container(Enterprise_Application, "Enterprise Application", "", "")        
            }
        }

        Deployment_Node(appRegistrations, "App Registrations") {
                    Container(helloworld_app, "helloworld-app", "Graph API permissions", "User.ReadWrite, Presence.ReadWrite")
        }
    }
    
    Rel(helloworld_app, Enterprise_Application, "Exposes as")
    Rel(React, Service_Principal, "Delegates to")
    Rel(Teams, React, "Provides endpoint with loginHint")
    BiRel(Service_Principal, Organizational_Resources, "Accesses")
    BiRel(React, Organizational_Resources, "On behalf of user")
    BiRel(Enterprise_Application, Service_Principal, "")

    UpdateElementStyle(azure, $borderColor="gray")
    UpdateElementStyle(github, $borderColor="gray")
    UpdateElementStyle(teams, $borderColor="gray")
    UpdateElementStyle(organizationalResources, $borderColor="gray")
    UpdateElementStyle(appRegistrations, $borderColor="gray")
    UpdateElementStyle(publishingOrganization, $borderColor="gray")
    
    UpdateRelStyle(helloworld_app, Enterprise_Application, $textColor="white", $lineColor="gray", ,$offsetX="5")
    UpdateRelStyle(React, Service_Principal, $textColor="white", $lineColor="gray", $offsetX="-40")
    UpdateRelStyle(Teams, React, $textColor="white", $lineColor="gray", $offsetY="-20", $offsetX="-90")
    UpdateRelStyle(Service_Principal, Organizational_Resources, $textColor="white", $lineColor="gray", $offsetY="-15", $offsetX="-40")
    UpdateRelStyle(React, Organizational_Resources, $textColor="white", $lineColor="gray", $offsetY="-15", $offsetX="-40")
    UpdateRelStyle(Enterprise_Application, Service_Principal, $textColor="white", $lineColor="gray")

The diagram above illustrates the process of registering an application on the Microsoft identity platform, configuring it for exposure to the publishing organization, and ultimately publishing the app within the Teams client.

Registering your application on Entra ID establishes its identity configuration, allowing it to integrate with the Microsoft identity platform. This registration means your app trusts the Microsoft identity platform to handle identity and access management tasks.

Following registration, an application object is created. It serves as a global identity configuration template, functioning as a tenant-wide, or even as a cross-tenant interface. Again, it’s an one-and-only global configuration template for the registered application, not a run-time instance of the application.

So, for an organization that wants to utilize identity resources within an application (whether it’s an SPA or a server-endorsed application), it needs a local representation of the registered application, ultimately to be configured with rich features of identity platform. That’s exactly what a service principal is for. The tenant where the app is registered has a service principal for the application, and any other tenants that grant access to the registered app will have their own service principals.

To sum up, an application registration has:

  • A one-to-one relationship with the software application (in our case, a vite-React SPA application that uses MSAL)
  • A one-to-many relationship with its corresponding service principal object(s) for tenants.

Now that we have set up the configuration for accessing our organizational resources, it’s time for MSAL in our React application to perform the authentication flow. Since it’s a hosted web application, a user can directly navigate to the entry endpoint and sign in. With the access token retrieved via MSAL, the application will then fetch the user’s profile and presence status.

A web application on a browser is fine, but we can take it a step further by integrating it directly into the organizational workplace on Teams. Since a Teams tab application is essentially a wrapper around an embedded web display, we need to make a slight modification to the authentication interaction. The Teams client blocks the login popup and does not allow for authentication redirection within the tab, so we’re going to authenticate users with SSO.

That was a bit of an explanation! With many abstract concepts and vague names in the context, it all sounds quite hazy. So let’s see things from a coder’s perspective, get our hands dirty, and translate these cloudy concepts of the Microsoft identity platform into our application code.

1. Head-first: MSAL in React.

Microsoft Authentication library (MSAL) is a set of well abstracted APIs, with support for multiple languages and platforms. Single Page Application in React is one of the supported platforms.

The first configuration to come up with after installing the @azure/msal-react package, is to create the instance of MSAL client in React application context.

1
2
3
4
5
6
7
8
9
10
11
12
13
const config = {
    auth: {
        clientId: "your-client-id-of-app-registrations",  
    },
};

const publicClientApplication = new PublicClientApplication(config);

ReactDOM.createRoot(document.getElementById('root')).render(
    <MsalProvider instance={publicClientApplication}>
        <App />
    </MsalProvider>
);

With the authentication client properly configured and integrated into the React project’s context, you are ready to use MSAL APIs for tasks such as triggering the login process, acquiring access tokens, and fetching user account information as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const UserProfile = () => {
    const { instance, inProgress, accounts } = useMsal();
    // ...

    useEffect(() => {
        const accessTokenRequest = {
            scopes: ["User.Read", "User.ReadWrite", "Presence.Read", "Presence.Read.All"],
            account: accounts[0],
        };

        instance
            .acquireTokenSilent(accessTokenRequest)
            .then((accessTokenResponse) => {
                const accessToken = accessTokenResponse.accessToken;

            // make remote call with the access token ... 
    }
}

The useMsal hook provides access to the MSAL instance, the progress state of authentication processes (inProgress), and the user accounts (accounts) that MSAL knows about.

The list of scopes specifies the API permissions that the Graph API will acts on. These permissions outline the specific data and functionalities that the application can access on behalf of the user, and the user will need to provide consent for these permissions during the authentication process.

The remote call to Graph API to fetch user information is done by REST API with access token we just retrieved by MSAL. For example, a logged-in user’s profile photo of Microsoft account can be fetched as below:

1
2
3
4
5
fetch("https://graph.microsoft.com/v1.0/me/photo/$value", {
    headers: {
        Authorization: `Bearer ${accessToken}`
    }
})

Another useful feature of the MSAL library is the AuthenticatedTemplate and UnauthenticatedTemplate components, which make conditional rendering based on authentication status extremely straightforward. For example, to render a sign-in button only when the user is unauthenticated, you would place it inside the UnauthenticatedTemplate component:

1
2
3
<UnauthenticatedTemplate>
    <button className="btn btn-outline-primary btn-sm" onClick={handleSignIn}>Sign In</button>
</UnauthenticatedTemplate>

Considering the application will be published on the organization’s Teams, it’s important to note that the Teams client doesn’t allow authentication redirection within tabs or the use of login popups. To address this limitation, I’ve opted for Single Sign-On (SSO) to handle the authentication process in a separate component with a mapped routing path.

The separate endpoint will be provided to Teams as a single tab entry, with user’s login hint (an email address) passed in as query parameter, in the format of following example. It’s also the exact tab URL that we register on Developer Portal for the application.

1
https://cynicdog.github.io/azure-entra-in-spa/#/teams?name={loginHint}

When a user accesses the application in Teams, the Teams client references the URL above. The React router captures this URL, extracts the login hint, and initiates the SSO process as below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const UserProfileOnTeams = () => {
    const { instance } = useMsal();

    useEffect(() => {
        const hash = window.location.hash;
        if (hash) {
            const urlParams = new URLSearchParams(hash.split('?')[1]);
            const nameParam = urlParams.get("name");
        }

        let loginHint = "";
        if (nameParam) {
            loginHint = nameParam; // This is the full email
        }

        // MSAL performs authentication with the parsed loginHint ...

When using SSO, the MSAL requires an application to explicitly initialize the MSAL instance before calling any APIs, so we need to implement such logic in our main entry file as below:

1
2
3
4
5
6
7
8
9
10
11
12
// create PublicClientApplication instance
const publicClientApplication = new PublicClientApplication(config);

const initializeMSAL = async () => {
    await publicClientApplication.initialize();            // let's explicitly initialize the MSAL client here!  

    ReactDOM.createRoot(document.getElementById('root')).render(
        <MsalProvider instance={publicClientApplication}>
            <App />
        </MsalProvider>
    );
};

These are the key points of using MSAL with React in a Single Page Application. Now, let’s deploy and host the application!

2. A Rare Giving Spirit: GitHub Actions and GitHub Pages.

There is a perfect fit for hosting our static SPA project: GitHub Pages, a static site hosting service provided by GitHub. The goal is to deploy our Vite-React project on GitHub Pages using GitHub Actions. GitHub Actions enables the automation of CI/CD pipelines, allowing for efficient build, test, and deployment processes. Another key reason of using GitHub Actions is to securely provide credentials, such as the application identifier (client id) generated during the application registration on Azure Entra ID, as runtime environment variables within the GitHub Actions workflow commands.

But let’s start with local deployment and set aside GitHub Actions for now.

The Node package gh-pages is a great tool that simplifies the process of publishing static files to GitHub Pages, so let’s set up the publishing environment for vite-React project locally.

When deployed on GitHub Pages, the base URL of for the web application is set to http(s)://<username>.github.io/<repository> by default, so we need to specify the base URL in our vite configuration accordingly, using /{PROJECT_REPOSITORY}/# as the value for the base URL attribute.

Notice that I suffixed the hash sign on the base URL.

Routing in Single Page Application can be a bit tricky, especially when deploying on static site hosts like GitHub Pages which is built for static sites, where every HTML, CSS, JS, image, etc., file is expected to be an actual file. On receiving the request of cynicdog.github.io/azure-entra-in-spa/teams?name={loginHint}, GitHub Pages will look for an index.html file in a directory called ./teams which doesn’t exist.

To overcome this issue, we’re going to integrate HashRouter from react-router-dom as below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { HashRouter as Router, Route, Routes } from 'react-router-dom';
const App = () => {

  return (
      <Router>
          <Routes>
              <Route path="/" element={<EntryPoint />} />
              <Route path="/export" element={<ExportView />} />
              <Route path="/teams" element={<UserProfileOnTeams />} />
          </Routes>
      </Router>
  )
}
export default App

HashRouter makes it possible to store the current location in the hash portion of the current URL, so it is never sent to the server. For example, when handling a URL entry of cynicdog.github.io/azure-entra-in-spa/#/teams?name={loginHint}, everything after the hash is processed by React, ultimately rendering UserProfileOnTeams component.

Then we set a full URL of our application home page deployed on GitHub Pages in package.json file and also add scripts to run gh-pages commands as below:

1
2
3
4
5
6
7
8
9
10
11
12
{
  // ... 
  "homepage": "https://cynicdog.github.io/azure-entra-in-spa/#",
  // ...
  "scripts": {
    // ...
    "predeploy": "npm run build",
    "deploy": "gh-pages -d dist"
    // ...
  }
  // ... 
}

Now we can run the deploy scripts, npm install predeploy; npm run deploy and gh-pages will perform the deployment process, however, I suggest not to run this command at the moment. Since the credentials are hardcoded in the code lines, they are going to be included in distribution files, and ultimately get exposed to public when published on web.

Curbing the enthusiasm, what we are going to do as a final step in deployment is to securely provide such credentials.

Executing CLI commands with securely provided secrets to deploy an application. Now that sounds like the perfect use case for GitHub Actions!

So I wrote a GitHub Action workflow that checks out the repository, install dependencies, then run the npm run deploy command as the final step, with providing credentials and configuration values as runtime environment variables. Secrets are stored as repository secrets, and GITHUB_TOKEN is given read-and-write permission.

First thing to point out in the workflow file is that we prefix all the environment variables with VITE_, since only variables prefixed with VITE_ are exposed to vite-processed codes:

1
2
 env:
      VITE_AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}

These variables will then retrieved in the code like below:

1
2
3
4
5
const config = {
    auth: {
        clientId: import.meta.env.VITE_AZURE_CLIENT_ID,
    },
};

Another thing to address in the workflow is to specify user identity and authentication details on Git context inside the workflow:

1
2
3
4
5
- name: Configure git to use HTTPS and set credentials
  run: |
    git config user.name "${{ github.actor }}"
    git config user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com"
    git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git

This command set the remote repository URL to include an access token, allowing the workflow to authenticate when pushing changes.

We can run the workflow manually on repository Actions tab. Upon completion, you will see a workflow run named pages-build-deployment will follow up the workflow we triggered. That’s a called workflow by gh-pages command, and once it completes successfully, you will see the application deployed on GitHub Pages. Below is the embedded view of the deployed application:

For the first time a user from an organization accesses the application, it requires an administrator’s consent, otherwise the information of the user will not be available.

That’s a well-functioning web application, and now we’re set to integrate it into the Teams client. One important takeaway is that the Teams client blocks the login popup initiated by MSAL, as well as any authentication redirection within the tab. To address this limitation, I came up with Single-Sign-On for the authentication. Ultimately, using SSO for authentication within Teams makes much more sense, as users are already signed in to the client.

There are two handshake points between systems in the scenario of publishing an app on Teams. The first handshake occurs between the published application on Teams and the hosted React web application, where the user’s login information from the Teams client is passed to the React app as a URL query parameter (i.e. ?name={loginHint}), as we’ve seen earlier.

The second handshake takes place between the published application on Teams and the application object on Entra ID. The bridge here is the Application ID URI, which serves as a globally unique identifier for the web API exposed by our application object to access through scopes. We need to link the Application ID URI with the Teams application on Developer Portal, so that a trust between them can be established, which is essential for ensuring SSO authentication flow.

flowchart TD
    subgraph Azure 
        A(App Registrations) 
    end 
    
    B[Teams]
    
    subgraph GitHub Pages
        C([React web app])
        D([MSAL])
    end 

    subgraph Publishing Organization 
        E(Organizational \nResources)
        F(Service \nPrincipal)
    end 

    G([user]) --> |email login hint|B

    A --- |A trust over Application ID URI|B
    A -. Sends Access Token at Runtime (SSO) .-> B
    B -. Provides endpoint with login hint .-> C
    C <--> D
    D <-- Graph API remote calls --> F
    E <--> F
    
    linkStyle 0,2,3,4,5,6 stroke-width:.3px;

One important note is that when you define an Application ID URI in App Registrations on the Azure Portal, you need to register a client identifier 5e3ce6c0-2b1f-4285-8d4b-75ee78787346, which is a unique value for Teams web client (see the full list of client IDs here). Additionally, the format of the Application ID URI should be api://{fully-qualified-domain-name.com}/{your-client-id-of-app-registrations}, where the domain is, in our case, {github-username}.github.io/{repository-name}.

On the publishing organization’s side in the flowchart above, there’s an entity that handles incoming Graph API requests for organizational resources: the service principal. We’ve previously discussed that a service principal is a local instance of the registered application.

The publishing organization is a tenant that grants permissions to the application and ultimately uses it. This tenant could be the one that originally registered the application in their Entra ID or it could be other tenants that want to use the application Each one of these tenant will have their own service principal for the application.

3. Closing.

This article has provided an in-depth exploration of integrating Azure Entra ID and the Microsoft Authentication Library (MSAL) in a React Single Page Application (SPA) hosted on GitHub Pages and published on Microsoft Teams. By using Azure’s identity management capabilities, we can authenticate users and access organizational resources via the Graph API.

The deployment of our SPA using GitHub Actions demonstrates a practical approach to continuous integration and secure deployment. As we integrate the application into Teams, the user experience with Single Sign-On (SSO) shows the effectiveness and flexibility of Microsoft’s identity platform.

This post is licensed under CC BY 4.0 by the author.