Tutorial: conectar usuários e adquirir um token para o Microsoft Graph em um aplicativo Web Node.js e Express

Neste tutorial, você irá compilar um aplicativo Web que credencia usuários e adquire tokens de acesso para chamar a Microsoft Graph. O aplicativo Web que você criará usa a MSAL (Biblioteca de Autenticação da Microsoft) para Node.

Siga as etapas deste tutorial para:

  • Registrar o aplicativo no portal do Azure
  • Criar um projeto de aplicativo Web Express
  • Instalar os pacotes da biblioteca de autenticação
  • Adicionar detalhes de registro do aplicativo
  • Adicionar o código para logon de usuário
  • Testar o aplicativo

Para saber mais, confira o código de exemplo que mostra como usar o Nó MSAL para entrar, sair e adquirir um token de acesso para um recurso protegido, como o Microsoft Graph.

Pré-requisitos

Registrar o aplicativo

Primeiro, conclua as etapas descritas em Registrar um aplicativo com a plataforma de identidade da Microsoft para registrar seu aplicativo.

Use as seguintes configurações para o registro do aplicativo:

  • Nome: ExpressWebApp (sugerido)
  • Tipos de conta compatíveis: Contas somente neste diretório organizacional
  • Tipo de plataforma: Web
  • URI de redirecionamento: http://localhost:3000/auth/redirect
  • Segredo do cliente: ********* (registre esse valor para uso em uma etapa posterior – ele é mostrado apenas uma vez)

Criar o projeto

Use a Ferramenta geradors de aplicativos Express para criar um esqueleto do aplicativo.

  1. Primeiramente, instale o pacote express-generator:
    npm install -g express-generator
  1. Em seguida, crie um esqueleto do aplicativo da seguinte maneira:
    express --view=hbs /ExpressWebApp && cd /ExpressWebApp
    npm install

Agora você tem um aplicativo Web do Express simples. A estrutura de arquivos e pastas do projeto será semelhante à seguinte estrutura de pasta:

ExpressWebApp/
├── bin/
|    └── wwww
├── public/
|    ├── images/
|    ├── javascript/
|    └── stylesheets/
|        └── style.css
├── routes/
|    ├── index.js
|    └── users.js
├── views/
|    ├── error.hbs
|    ├── index.hbs
|    └── layout.hbs
├── app.js
└── package.json

Instalar a biblioteca de autenticação

Localize a raiz do diretório do projeto em um terminal e instale o pacote da MSAL Node por meio do npm.

    npm install --save @azure/msal-node

Instalar outras dependências

A amostra do aplicativo Web neste tutorial usa o pacote express-session para gerenciamento de sessão, pacote dotenv para ler parâmetros de ambiente durante o desenvolvimento e axios para fazer chamadas de rede para a API do Microsoft Graph. Instale-os por meio de npm:

    npm install --save express-session dotenv axios

Adicionar detalhes de registro do aplicativo

  1. Crie um arquivo .env.dev na raiz da pasta do projeto. Em seguida, adicione o seguinte código:
CLOUD_INSTANCE="Enter_the_Cloud_Instance_Id_Here" # cloud instance string should end with a trailing slash
TENANT_ID="Enter_the_Tenant_Info_Here"
CLIENT_ID="Enter_the_Application_Id_Here"
CLIENT_SECRET="Enter_the_Client_Secret_Here"

REDIRECT_URI="http://localhost:3000/auth/redirect"
POST_LOGOUT_REDIRECT_URI="http://localhost:3000"

GRAPH_API_ENDPOINT="Enter_the_Graph_Endpoint_Here" # graph api endpoint string should end with a trailing slash

EXPRESS_SESSION_SECRET="Enter_the_Express_Session_Secret_Here"

Preencha esses detalhes com os valores obtidos do portal de registro de aplicativo do Azure:

  • Enter_the_Cloud_Instance_Id_Here: a instância de nuvem do Azure na qual o aplicativo está registrado.
    • Para a nuvem principal (ou global) do Azure, insira https://login.microsoftonline.com/ (inclua a barra de avanço à direita).
    • Para nuvens nacionais (por exemplo, China), você pode encontrar os valores apropriados em Nuvens nacionais.
  • Enter_the_Tenant_Info_here deve ser um destes parâmetros:
    • Se o aplicativo tem suporte para contas neste diretório organizacional, substitua esse valor pela ID do locatário ou pelo Nome do locatário. Por exemplo, contoso.microsoft.com.
    • Se o aplicativo tem suporte para contas em qualquer diretório organizacional, substitua esse valor por organizations.
    • Se o seu aplicativo tem suporte para contas em qualquer diretório organizacional e contas pessoais Microsoft, substitua esse valor por common.
    • Para restringir o suporte a contas pessoais da Microsoft, substitua esse valor por consumers.
  • Enter_the_Application_Id_Here: a ID do Aplicativo (cliente) do aplicativo que você registrou.
  • Enter_the_Client_secret: substitua esse valor pelo segredo do cliente criado anteriormente. Para gerar uma chave, use Certificados e segredos nas configurações de registro do aplicativo no portal do Azure.

Aviso

Qualquer segredo de texto não criptografado no código-fonte representa um risco de segurança maior. Este artigo usa um segredo do cliente de texto não criptografado apenas para fins de simplicidade. Use credenciais de certificado em vez de segredos do cliente nos seus aplicativos cliente confidenciais, especialmente os aplicativos que você pretende implantar na produção.

  • Enter_the_Graph_Endpoint_Here: a instância de nuvem da API do Microsoft Graph que seu aplicativo chamará. Para o serviço da API do Microsoft Graph principal (global), insira https://graph.microsoft.com/ (inclua a barra à direita).
  • Enter_the_Express_Session_Secret_Here o segredo usado para assinar o cookie da sessão do Express. Escolha uma cadeia de caracteres aleatória para substituir essa cadeia de caracteres, como o segredo do cliente.
  1. Em seguida, crie um arquivo chamado authConfig.js na raiz do projeto para leitura nesses parâmetros. Depois de criado, adicione o seguinte código:
/*
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */

require('dotenv').config({ path: '.env.dev' });

/**
 * Configuration object to be passed to MSAL instance on creation.
 * For a full list of MSAL Node configuration parameters, visit:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/configuration.md
 */
const msalConfig = {
    auth: {
        clientId: process.env.CLIENT_ID, // 'Application (client) ID' of app registration in Azure portal - this value is a GUID
        authority: process.env.CLOUD_INSTANCE + process.env.TENANT_ID, // Full directory URL, in the form of https://login.microsoftonline.com/<tenant>
        clientSecret: process.env.CLIENT_SECRET // Client secret generated from the app registration in Azure portal
    },
    system: {
        loggerOptions: {
            loggerCallback(loglevel, message, containsPii) {
                console.log(message);
            },
            piiLoggingEnabled: false,
            logLevel: 3,
        }
    }
}

const REDIRECT_URI = process.env.REDIRECT_URI;
const POST_LOGOUT_REDIRECT_URI = process.env.POST_LOGOUT_REDIRECT_URI;
const GRAPH_ME_ENDPOINT = process.env.GRAPH_API_ENDPOINT + "v1.0/me";

module.exports = {
    msalConfig,
    REDIRECT_URI,
    POST_LOGOUT_REDIRECT_URI,
    GRAPH_ME_ENDPOINT
};

Adicionar código para entrada do usuário e aquisição de token

  1. Crie uma nova pasta chamada autenticação e adicione um novo arquivo chamado AuthProvider.js nela. Isso conterá a classe AuthProvider, que encapsula a lógica de autenticação necessária usando o MSAL Node. Adicione o seguinte código a ele:
const msal = require('@azure/msal-node');
const axios = require('axios');

const { msalConfig } = require('../authConfig');

class AuthProvider {
    msalConfig;
    cryptoProvider;

    constructor(msalConfig) {
        this.msalConfig = msalConfig
        this.cryptoProvider = new msal.CryptoProvider();
    };

    login(options = {}) {
        return async (req, res, next) => {

            /**
             * MSAL Node library allows you to pass your custom state as state parameter in the Request object.
             * The state parameter can also be used to encode information of the app's state before redirect.
             * You can pass the user's state in the app, such as the page or view they were on, as input to this parameter.
             */
            const state = this.cryptoProvider.base64Encode(
                JSON.stringify({
                    successRedirect: options.successRedirect || '/',
                })
            );

            const authCodeUrlRequestParams = {
                state: state,

                /**
                 * By default, MSAL Node will add OIDC scopes to the auth code url request. For more information, visit:
                 * https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
                 */
                scopes: options.scopes || [],
                redirectUri: options.redirectUri,
            };

            const authCodeRequestParams = {
                state: state,

                /**
                 * By default, MSAL Node will add OIDC scopes to the auth code request. For more information, visit:
                 * https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
                 */
                scopes: options.scopes || [],
                redirectUri: options.redirectUri,
            };

            /**
             * If the current msal configuration does not have cloudDiscoveryMetadata or authorityMetadata, we will 
             * make a request to the relevant endpoints to retrieve the metadata. This allows MSAL to avoid making 
             * metadata discovery calls, thereby improving performance of token acquisition process. For more, see:
             * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/performance.md
             */
            if (!this.msalConfig.auth.cloudDiscoveryMetadata || !this.msalConfig.auth.authorityMetadata) {

                const [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all([
                    this.getCloudDiscoveryMetadata(this.msalConfig.auth.authority),
                    this.getAuthorityMetadata(this.msalConfig.auth.authority)
                ]);

                this.msalConfig.auth.cloudDiscoveryMetadata = JSON.stringify(cloudDiscoveryMetadata);
                this.msalConfig.auth.authorityMetadata = JSON.stringify(authorityMetadata);
            }

            const msalInstance = this.getMsalInstance(this.msalConfig);

            // trigger the first leg of auth code flow
            return this.redirectToAuthCodeUrl(
                authCodeUrlRequestParams,
                authCodeRequestParams,
                msalInstance
            )(req, res, next);
        };
    }

    acquireToken(options = {}) {
        return async (req, res, next) => {
            try {
                const msalInstance = this.getMsalInstance(this.msalConfig);

                /**
                 * If a token cache exists in the session, deserialize it and set it as the 
                 * cache for the new MSAL CCA instance. For more, see: 
                 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md
                 */
                if (req.session.tokenCache) {
                    msalInstance.getTokenCache().deserialize(req.session.tokenCache);
                }

                const tokenResponse = await msalInstance.acquireTokenSilent({
                    account: req.session.account,
                    scopes: options.scopes || [],
                });

                /**
                 * On successful token acquisition, write the updated token 
                 * cache back to the session. For more, see: 
                 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md
                 */
                req.session.tokenCache = msalInstance.getTokenCache().serialize();
                req.session.accessToken = tokenResponse.accessToken;
                req.session.idToken = tokenResponse.idToken;
                req.session.account = tokenResponse.account;

                res.redirect(options.successRedirect);
            } catch (error) {
                if (error instanceof msal.InteractionRequiredAuthError) {
                    return this.login({
                        scopes: options.scopes || [],
                        redirectUri: options.redirectUri,
                        successRedirect: options.successRedirect || '/',
                    })(req, res, next);
                }

                next(error);
            }
        };
    }

    handleRedirect(options = {}) {
        return async (req, res, next) => {
            if (!req.body || !req.body.state) {
                return next(new Error('Error: response not found'));
            }

            const authCodeRequest = {
                ...req.session.authCodeRequest,
                code: req.body.code,
                codeVerifier: req.session.pkceCodes.verifier,
            };

            try {
                const msalInstance = this.getMsalInstance(this.msalConfig);

                if (req.session.tokenCache) {
                    msalInstance.getTokenCache().deserialize(req.session.tokenCache);
                }

                const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body);

                req.session.tokenCache = msalInstance.getTokenCache().serialize();
                req.session.idToken = tokenResponse.idToken;
                req.session.account = tokenResponse.account;
                req.session.isAuthenticated = true;

                const state = JSON.parse(this.cryptoProvider.base64Decode(req.body.state));
                res.redirect(state.successRedirect);
            } catch (error) {
                next(error);
            }
        }
    }

    logout(options = {}) {
        return (req, res, next) => {

            /**
             * Construct a logout URI and redirect the user to end the
             * session with Azure AD. For more information, visit:
             * https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
             */
            let logoutUri = `${this.msalConfig.auth.authority}/oauth2/v2.0/`;

            if (options.postLogoutRedirectUri) {
                logoutUri += `logout?post_logout_redirect_uri=${options.postLogoutRedirectUri}`;
            }

            req.session.destroy(() => {
                res.redirect(logoutUri);
            });
        }
    }

    /**
     * Instantiates a new MSAL ConfidentialClientApplication object
     * @param msalConfig: MSAL Node Configuration object 
     * @returns 
     */
    getMsalInstance(msalConfig) {
        return new msal.ConfidentialClientApplication(msalConfig);
    }


    /**
     * Prepares the auth code request parameters and initiates the first leg of auth code flow
     * @param req: Express request object
     * @param res: Express response object
     * @param next: Express next function
     * @param authCodeUrlRequestParams: parameters for requesting an auth code url
     * @param authCodeRequestParams: parameters for requesting tokens using auth code
     */
    redirectToAuthCodeUrl(authCodeUrlRequestParams, authCodeRequestParams, msalInstance) {
        return async (req, res, next) => {
            // Generate PKCE Codes before starting the authorization flow
            const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();

            // Set generated PKCE codes and method as session vars
            req.session.pkceCodes = {
                challengeMethod: 'S256',
                verifier: verifier,
                challenge: challenge,
            };

            /**
             * By manipulating the request objects below before each request, we can obtain
             * auth artifacts with desired claims. For more information, visit:
             * https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationurlrequest
             * https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationcoderequest
             **/
            req.session.authCodeUrlRequest = {
                ...authCodeUrlRequestParams,
                responseMode: msal.ResponseMode.FORM_POST, // recommended for confidential clients
                codeChallenge: req.session.pkceCodes.challenge,
                codeChallengeMethod: req.session.pkceCodes.challengeMethod,
            };

            req.session.authCodeRequest = {
                ...authCodeRequestParams,
                code: '',
            };

            try {
                const authCodeUrlResponse = await msalInstance.getAuthCodeUrl(req.session.authCodeUrlRequest);
                res.redirect(authCodeUrlResponse);
            } catch (error) {
                next(error);
            }
        };
    }

    /**
     * Retrieves cloud discovery metadata from the /discovery/instance endpoint
     * @returns 
     */
    async getCloudDiscoveryMetadata(authority) {
        const endpoint = 'https://login.microsoftonline.com/common/discovery/instance';

        try {
            const response = await axios.get(endpoint, {
                params: {
                    'api-version': '1.1',
                    'authorization_endpoint': `${authority}/oauth2/v2.0/authorize`
                }
            });

            return await response.data;
        } catch (error) {
            throw error;
        }
    }

    /**
     * Retrieves oidc metadata from the openid endpoint
     * @returns
     */
    async getAuthorityMetadata(authority) {
        const endpoint = `${authority}/v2.0/.well-known/openid-configuration`;

        try {
            const response = await axios.get(endpoint);
            return await response.data;
        } catch (error) {
            console.log(error);
        }
    }
}

const authProvider = new AuthProvider(msalConfig);

module.exports = authProvider;
  1. Em seguida, crie um novo arquivo chamado auth.js na pasta de rotas e adicione o seguinte código:
/*
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */

var express = require('express');

const authProvider = require('../auth/AuthProvider');
const { REDIRECT_URI, POST_LOGOUT_REDIRECT_URI } = require('../authConfig');

const router = express.Router();

router.get('/signin', authProvider.login({
    scopes: [],
    redirectUri: REDIRECT_URI,
    successRedirect: '/'
}));

router.get('/acquireToken', authProvider.acquireToken({
    scopes: ['User.Read'],
    redirectUri: REDIRECT_URI,
    successRedirect: '/users/profile'
}));

router.post('/redirect', authProvider.handleRedirect());

router.get('/signout', authProvider.logout({
    postLogoutRedirectUri: POST_LOGOUT_REDIRECT_URI
}));

module.exports = router;
  1. Em seguida, atualize a rota index.js substituindo o código existente pelo seguinte trecho de código:
/*
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */

var express = require('express');
var router = express.Router();

router.get('/', function (req, res, next) {
    res.render('index', {
        title: 'MSAL Node & Express Web App',
        isAuthenticated: req.session.isAuthenticated,
        username: req.session.account?.username,
    });
});

module.exports = router;
  1. Por fim, atualize a rota users.js substituindo o código existente pelo seguinte trecho de código:
/*
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */

var express = require('express');
var router = express.Router();

var fetch = require('../fetch');

var { GRAPH_ME_ENDPOINT } = require('../authConfig');

// custom middleware to check auth state
function isAuthenticated(req, res, next) {
    if (!req.session.isAuthenticated) {
        return res.redirect('/auth/signin'); // redirect to sign-in route
    }

    next();
};

router.get('/id',
    isAuthenticated, // check if user is authenticated
    async function (req, res, next) {
        res.render('id', { idTokenClaims: req.session.account.idTokenClaims });
    }
);

router.get('/profile',
    isAuthenticated, // check if user is authenticated
    async function (req, res, next) {
        try {
            const graphResponse = await fetch(GRAPH_ME_ENDPOINT, req.session.accessToken);
            res.render('profile', { profile: graphResponse });
        } catch (error) {
            next(error);
        }
    }
);

module.exports = router;

Adicionar código para chamada da API do Microsoft Graph

Crie um arquivo chamado fetch.js na raiz do seu projeto e adicione o seguinte código:

/*
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */

var axios = require('axios');

/**
 * Attaches a given access token to a MS Graph API call
 * @param endpoint: REST API endpoint to call
 * @param accessToken: raw access token string
 */
async function fetch(endpoint, accessToken) {
    const options = {
        headers: {
            Authorization: `Bearer ${accessToken}`
        }
    };

    console.log(`request made to ${endpoint} at: ` + new Date().toString());

    try {
        const response = await axios.get(endpoint, options);
        return await response.data;
    } catch (error) {
        throw new Error(error);
    }
}

module.exports = fetch;

Adicionar exibições para exibir dados

  1. Na pasta de exibições, atualize o arquivo index.hbs substituindo o código existente pelo seguinte:
<h1>{{title}}</h1>
{{#if isAuthenticated }}
<p>Hi {{username}}!</p>
<a href="/users/id">View ID token claims</a>
<br>
<a href="/auth/acquireToken">Acquire a token to call the Microsoft Graph API</a>
<br>
<a href="/auth/signout">Sign out</a>
{{else}}
<p>Welcome to {{title}}</p>
<a href="/auth/signin">Sign in</a>
{{/if}}
  1. Ainda na mesma pasta, crie outro arquivo chamado id.hbs para exibir o conteúdo do token da ID do usuário:
<h1>Azure AD</h1>
<h3>ID Token</h3>
<table>
    <tbody>
        {{#each idTokenClaims}}
        <tr>
            <td>{{@key}}</td>
            <td>{{this}}</td>
        </tr>
        {{/each}}
    </tbody>
</table>
<br>
<a href="https://aka.ms/id-tokens" target="_blank">Learn about claims in this ID token</a>
<br>
<a href="/">Go back</a>
  1. Por fim, crie outro arquivo chamado profile.hbs para exibir o resultado da chamada feita ao Microsoft Graph:
<h1>Microsoft Graph API</h1>
<h3>/me endpoint response</h3>
<table>
    <tbody>
        {{#each profile}}
        <tr>
            <td>{{@key}}</td>
            <td>{{this}}</td>
        </tr>
        {{/each}}
    </tbody>
</table>
<br>
<a href="/">Go back</a>

Registrar roteadores e adicionar gerenciamento de estado

No arquivo app.js na raiz da pasta do projeto, registre as rotas criadas anteriormente e adicione suporte de sessão para acompanhar o estado de autenticação usando o pacote express-session. Substitua o código existente pelo seguinte trecho de código:

/*
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */

require('dotenv').config();

var path = require('path');
var express = require('express');
var session = require('express-session');
var createError = require('http-errors');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var authRouter = require('./routes/auth');

// initialize express
var app = express();

/**
 * Using express-session middleware for persistent user session. Be sure to
 * familiarize yourself with available options. Visit: https://www.npmjs.com/package/express-session
 */
 app.use(session({
    secret: process.env.EXPRESS_SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
        httpOnly: true,
        secure: false, // set this to true on production
    }
}));

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'hbs');

app.use(logger('dev'));
app.use(express.json());
app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/auth', authRouter);

// catch 404 and forward to error handler
app.use(function (req, res, next) {
    next(createError(404));
});

// error handler
app.use(function (err, req, res, next) {
    // set locals, only providing error in development
    res.locals.message = err.message;
    res.locals.error = req.app.get('env') === 'development' ? err : {};

    // render the error page
    res.status(err.status || 500);
    res.render('error');
});

module.exports = app;

Testar a conexão e chamada ao Microsoft Graph

Você concluiu a criação do aplicativo e agora está pronto para testar a funcionalidade do aplicativo.

  1. Inicie o aplicativo de console Node.js executando o seguinte comando na raiz da pasta do seu projeto:
   npm start
  1. Abra uma janela do navegador e navegue até http://localhost:3000. Você deverá ver uma página inicial:

Web app welcome page displaying

  1. Selecione o link Entrar. Você deve ver a tela de entrada do Microsoft Entra:

Microsoft Entra sign-in screen displaying

  1. Depois de inserir suas credenciais, você verá uma tela de consentimento solicitando a aprovação das permissões para o aplicativo.

Microsoft Entra consent screen displaying

  1. Depois de consentir, você será redirecionado para a página inicial do aplicativo.

Web app welcome page after sign-in displaying

  1. Selecione o link Exibir token de ID para exibir o conteúdo do token de ID do usuário conectado.

User ID token screen displaying

  1. Volte para a página inicial e selecione o link Adquirir um token de acesso e chamar a API do Microsoft Graph. Depois de fazer isso, a resposta do ponto de extremidade do Microsoft Graph /me do usuário conectado será exibida.

Graph call screen displaying

  1. Retorne à página inicial e selecione o link Sair. Você deve ver a tela de logoff do Microsoft Entra.

Microsoft Entra sign-out screen displaying

Como o aplicativo funciona

Nesse tutorial, você criou uma instância para um objeto ConfidentialClientApplication da MSAL Node transmitindo-o um objeto de configuração (msalConfig) que contém os parâmetros obtidos do seu registro de aplicativo do Microsoft Entra no portal do Azure. O aplicativo Web que você criou usa o protocolo do OpenID Connect para conectar usuários e o fluxo de código de autorização de OAuth 2.0 para obter os tokens de acesso.

Próximas etapas

Caso deseje se aprofundar no desenvolvimento de aplicativos Web Node.js e Express na plataforma de identidade da Microsoft, confira nossa série de cenários de várias partes: