Tutorial: Inicie sessão em utilizadores e adquira um token para o Microsoft Graph numa aplicação Web Node.js & Express

Neste tutorial, você cria um aplicativo Web que entra em usuários e adquire tokens de acesso para chamar o Microsoft Graph. O aplicativo Web que você cria usa a Biblioteca de Autenticação da Microsoft (MSAL) para o Nó.

Siga as etapas neste 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 registo da aplicação
  • Adicionar código para login de usuário
  • Testar a aplicação

Para obter mais informações, consulte 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

Registar a candidatura

Primeiro, conclua as etapas 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 suportados: Somente contas 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 Express application generator para criar um esqueleto de aplicativo.

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

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

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 seu projeto em um terminal e instale o pacote MSAL Node via npm.

    npm install --save @azure/msal-node

Instalar outras dependências

O exemplo de aplicativo Web neste tutorial usa o pacote de sessão expressa para gerenciamento de sessão, pacote dotenv para leitura de parâmetros de ambiente durante o desenvolvimento e axios para fazer chamadas de rede para a API do Microsoft Graph. Instale estes via npm:

    npm install --save express-session dotenv axios

Adicionar detalhes de registo da aplicação

  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 estes detalhes com os valores que você obtém do portal de registro de aplicativo do Azure:

  • Enter_the_Cloud_Instance_Id_Here: A instância de nuvem do Azure na qual seu aplicativo está registrado.
    • Para a nuvem principal (ou global) do Azure, insira https://login.microsoftonline.com/ (inclua a barra à direita).
    • Para nuvens nacionais (por exemplo, China), você pode encontrar valores apropriados em Nuvens nacionais.
  • Enter_the_Tenant_Info_here deve ser um dos seguintes parâmetros:
    • Se o seu aplicativo oferecer suporte a contas nesse diretório organizacional, substitua esse valor pelo ID do Locatário ou Nome do Locatário. Por exemplo, contoso.microsoft.com.
    • Se seu aplicativo oferecer suporte a contas em qualquer diretório organizacional, substitua esse valor por organizations.
    • Se o seu aplicativo oferecer suporte a contas em qualquer diretório organizacional e contas pessoais da Microsoft, substitua esse valor por common.
    • Para restringir o suporte apenas a contas pessoais da Microsoft, substitua este valor por consumers.
  • Enter_the_Application_Id_Here: O 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 nova chave, use Certificados & segredos nas configurações de registro do aplicativo no portal do Azure.

Aviso

Qualquer segredo de texto simples no código-fonte representa um risco de segurança acrescido. Este artigo usa um segredo de cliente de texto simples apenas para simplificar. Use credenciais de certificado em vez de segredos de cliente em seus aplicativos cliente confidenciais, especialmente aqueles 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 principal (global) da API do Microsoft Graph, insira https://graph.microsoft.com/ (inclua a barra à direita).
  • Enter_the_Express_Session_Secret_Here o segredo usado para assinar o cookie de sessão Express. Escolha uma cadeia aleatória de caracteres para substituir essa cadeia de caracteres, como o segredo do cliente.
  1. Em seguida, crie um arquivo chamado authConfig.js na raiz do seu projeto para leitura nesses parâmetros. Uma vez criado, adicione o seguinte código lá:
/*
 * 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 login de usuário e aquisição de token

  1. Crie uma nova pasta chamada auth e adicione um novo arquivo chamado AuthProvider.js sob ela. Isso conterá a classe AuthProvider, que encapsula a lógica de autenticação necessária usando o nó MSAL. Adicione o seguinte código lá:
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 lá:
/*
 * 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. 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. Finalmente, 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 chamar a 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 vistas para apresentar dados

  1. Na pasta views, 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 de 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. Finalmente, 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 controlar o estado de autenticação usando o pacote de sessão expressa. Substitua o código existente com o 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 o início de sessão e chamar o 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 do Node.js executando o seguinte comando de dentro da raiz da pasta do projeto:
   npm start
  1. Abra uma janela do browser e navegue para http://localhost:3000. Você deve ver uma página de boas-vindas:

Web app welcome page displaying

  1. Selecione Link Entrar . Deverá ver o ecrã de início de sessão do Microsoft Entra:

Microsoft Entra sign-in screen displaying

  1. Depois de inserir suas credenciais, você verá uma tela de consentimento solicitando que você aprove as permissões para o aplicativo.

Microsoft Entra consent screen displaying

  1. Depois de consentir, você deve ser redirecionado de volta 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 chame a API do Microsoft Graph. Depois disso, você verá a resposta do ponto de extremidade /me do Microsoft Graph para o usuário conectado.

Graph call screen displaying

  1. Volte para a página inicial e selecione o link Sair . Você deve ver a tela de saída do Microsoft Entra.

Microsoft Entra sign-out screen displaying

Como funciona a aplicação

Neste tutorial, você instanciou um objeto ConfidentialClientApplication do nó MSAL passando-lhe um objeto de configuração (msalConfig) que contém parâmetros obtidos do registro do aplicativo Microsoft Entra no portal do Azure. O aplicativo Web que você criou usa o protocolo OpenID Connect para entrar usuários e o fluxo de código de autorização OAuth 2.0 para obter tokens de acesso.

Próximos passos

Se você quiser se aprofundar no desenvolvimento de aplicativos Web Node.js & Express na plataforma de identidade da Microsoft, consulte nossa série de cenários com várias partes: