Partilhar via


Tutorial: Adicionar início de sessão a uma aplicação Web Node/Express.js utilizando a plataforma de identidade da Microsoft

Aplica-se a: círculo verde com um símbolo de marca de verificação branco. Inquilinos da força de trabalho círculo verde com um símbolo de marca de verificação branco. Inquilinos externos (saiba mais)

Neste tutorial, você adiciona lógica de entrada e saída ao seu aplicativo Web Node/Express. Esse código permite que você faça login de usuários em seu aplicativo voltado para o cliente em um locatário externo ou funcionários em um locatário da força de trabalho.

Este tutorial é a parte 2 da série de tutoriais de 3 partes.

Neste tutorial, você:

  • Adicionar lógica de entrada e saída
  • Exibir declarações de token de ID
  • Execute o aplicativo e teste a experiência de entrada e saída.

Pré-requisitos

Criar objeto de configuração MSAL

No editor de códigos, abra authConfig.js arquivo e adicione o seguinte código:

require('dotenv').config();

const TENANT_SUBDOMAIN = process.env.TENANT_SUBDOMAIN || 'Enter_the_Tenant_Subdomain_Here';
const REDIRECT_URI = process.env.REDIRECT_URI || 'http://localhost:3000/auth/redirect';
const POST_LOGOUT_REDIRECT_URI = process.env.POST_LOGOUT_REDIRECT_URI || 'http://localhost:3000';
const GRAPH_ME_ENDPOINT = process.env.GRAPH_API_ENDPOINT + "v1.0/me" || 'Enter_the_Graph_Endpoint_Here';

/**
 * 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 || 'Enter_the_Application_Id_Here', // 'Application (client) ID' of app registration in Azure portal - this value is a GUID
        //For external tenant
        authority: process.env.AUTHORITY || `https://${TENANT_SUBDOMAIN}.ciamlogin.com/`, // replace "Enter_the_Tenant_Subdomain_Here" with your tenant name
        //For workforce tenant
        //authority: process.env.CLOUD_INSTANCE + process.env.TENANT_ID
        clientSecret: process.env.CLIENT_SECRET || 'Enter_the_Client_Secret_Here', // Client secret generated from the app registration in Azure portal
    },
    system: {
        loggerOptions: {
            loggerCallback(loglevel, message, containsPii) {
                console.log(message);
            },
            piiLoggingEnabled: false,
            logLevel: 'Info',
        },
    },
};

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

O objeto msalConfig contém um conjunto de opções de configuração que você usa para personalizar o comportamento de seus fluxos de autenticação.

No ficheiro authConfig.js, substitua:

  • Enter_the_Application_Id_Here com o ID do aplicativo (cliente) do aplicativo que você registrou anteriormente.

  • Enter_the_Tenant_Subdomain_Here e substitua-o pelo subdomínio Diretório (locatário) externo. Por exemplo, se o domínio principal do locatário for contoso.onmicrosoft.com, use contoso. Se não tiver o nome do inquilino, saiba como ler os detalhes do inquilino. Esse valor é necessário apenas para o locatário externo.

  • Enter_the_Client_Secret_Here com o valor secreto do aplicativo copiado anteriormente.

  • Enter_the_Graph_Endpoint_Here com a instância de nuvem da API do Microsoft Graph que a sua aplicação chamará. Use o valor https://graph.microsoft.com/ (inclua a barra invertida)

Se usar o ficheiro .env para armazenar as suas informações de configuração:

  1. No editor de códigos, abra arquivo .env e adicione o código a seguir.

        CLIENT_ID=Enter_the_Application_Id_Here
        TENANT_SUBDOMAIN=Enter_the_Tenant_Subdomain_Here 
        CLOUD_INSTANCE="Enter_the_Cloud_Instance_Id_Here" # cloud instance string should end with a trailing slash
        TENANT_ID=Enter_the_Tenant_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 # express session secret, just any random text
    
  2. Substitua o espaço reservado por um marcador:

    1. Enter_the_Application_Id_Here, Enter_the_Tenant_Subdomain_Here e Enter_the_Client_Secret_Here como explicado anteriormente.
    2. Enter_the_Cloud_Instance_Id_Here com a instância de nuvem do Azure na qual o seu aplicativo está registado. Utilize https://login.microsoftonline.com/ como seu valor (inclua a barra diagonal final). Este valor é necessário apenas para o inquilino da força de trabalho.
    3. Enter_the_Tenant_ID_here com o ID do locatário da força de trabalho ou o domínio principal, como aaaabbbb-0000-cccc-1111-dddd2222eeee ou contoso.microsoft.com. Este valor é necessário apenas para o inquilino da força de trabalho.

Você exporta msalConfig, REDIRECT_URI, TENANT_SUBDOMAIN, GRAPH_ME_ENDPOINT e POST_LOGOUT_REDIRECT_URI variáveis no arquivo authConfig.js para torná-las acessíveis em outros arquivos.

URL de autoridade para seu aplicativo

As autoridades responsáveis pelas aplicações para inquilinos externos e da força de trabalho apresentam-se de forma diferente. Construa-os como mostrado abaixo:

//Authority for workforce tenant
authority: process.env.CLOUD_INSTANCE + process.env.TENANT_ID

Usar domínio de URL personalizado (opcional)

Domínios de URL personalizados não são suportados em inquilinos da força de trabalho.

Adicionar rotas expressas

As rotas do Express fornecem os pontos de extremidade que nos permitem executar operações como iniciar sessão, terminar sessão e visualizar declarações de token de ID.

Ponto de entrada do aplicativo

No editor de código, abra o arquivo routes/index.js e adicione o seguinte código:

const express = require('express');
const 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 !== '' ? req.session.account?.username : req.session.account?.name,
    });
});    
module.exports = router;

A rota / é o ponto de entrada para o aplicativo. Ele renderiza a views/index.hbs exibição que você criou anteriormente em componentes da interface do usuário do aplicativo Build. isAuthenticated é uma variável booleana que determina o que você vê na exibição.

Entrar e sair

  1. No editor de código, abra o ficheiro routes/auth.js e adicione o seguinte código:

    const express = require('express');
    const authController = require('../controller/authController');
    const router = express.Router();
    
    router.get('/signin', authController.signIn);
    router.get('/signout', authController.signOut);
    router.post('/redirect', authController.handleRedirect);
    
    module.exports = router;
    
  2. No editor de código, abra o ficheiro controller/authController.js e adicione o seguinte código:

    const authProvider = require('../auth/AuthProvider');
    
    exports.signIn = async (req, res, next) => {
        return authProvider.login(req, res, next);
    };
    
    exports.handleRedirect = async (req, res, next) => {
        return authProvider.handleRedirect(req, res, next);
    }
    
    exports.signOut = async (req, res, next) => {
        return authProvider.logout(req, res, next);
    };
    
    
  3. No seu editor de código, abra arquivo auth/AuthProvider.js e adicione seguinte código:

    const msal = require('@azure/msal-node');
    const axios = require('axios');
    const { msalConfig, TENANT_SUBDOMAIN, REDIRECT_URI, POST_LOGOUT_REDIRECT_URI, GRAPH_ME_ENDPOINT} = require('../authConfig');
    
    class AuthProvider {
        config;
        cryptoProvider;
    
        constructor(config) {
            this.config = config;
            this.cryptoProvider = new msal.CryptoProvider();
        }
    
        getMsalInstance(msalConfig) {
            return new msal.ConfidentialClientApplication(msalConfig);
        }
    
        async login(req, res, next, options = {}) {
            // create a GUID for crsf
            req.session.csrfToken = this.cryptoProvider.createNewGuid();
    
            /**
             * The 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({
                    csrfToken: req.session.csrfToken,
                    redirectTo: '/',
                })
            );
    
            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: [],
            };
    
            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: [],
            };
    
            /**
             * 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.
             */
            if (!this.config.msalConfig.auth.authorityMetadata) {
                const authorityMetadata = await this.getAuthorityMetadata();
                this.config.msalConfig.auth.authorityMetadata = JSON.stringify(authorityMetadata);
            }
    
            const msalInstance = this.getMsalInstance(this.config.msalConfig);
    
            // trigger the first leg of auth code flow
            return this.redirectToAuthCodeUrl(
                req,
                res,
                next,
                authCodeUrlRequestParams,
                authCodeRequestParams,
                msalInstance
            );
        }
    
        async handleRedirect(req, res, next) {
            const authCodeRequest = {
                ...req.session.authCodeRequest,
                code: req.body.code, // authZ code
                codeVerifier: req.session.pkceCodes.verifier, // PKCE Code Verifier
            };
    
            try {
                const msalInstance = this.getMsalInstance(this.config.msalConfig);
                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.redirectTo);
            } catch (error) {
                next(error);
            }
        }
    
        async logout(req, res, next) {
            /**
             * Construct a logout URI and redirect the user to end the
             * session with Microsoft Entra ID. For more information, visit:
             * https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
             */
            //For external tenant
            //const logoutUri = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=${this.config.postLogoutRedirectUri}`;
    
            //For workforce tenant
            let logoutUri = `${this.config.msalConfig.auth.authority}/oauth2/v2.0/logout?post_logout_redirect_uri=${this.config.postLogoutRedirectUri}`;
            req.session.destroy(() => {
                res.redirect(logoutUri);
            });
        }
    
        /**
         * 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
         */
        async redirectToAuthCodeUrl(req, res, next, authCodeUrlRequestParams, authCodeRequestParams, msalInstance) {
            // 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,
                redirectUri: this.config.redirectUri,
                responseMode: 'form_post', // recommended for confidential clients
                codeChallenge: req.session.pkceCodes.challenge,
                codeChallengeMethod: req.session.pkceCodes.challengeMethod,
            };
    
            req.session.authCodeRequest = {
                ...authCodeRequestParams,
                redirectUri: this.config.redirectUri,
                code: '',
            };
    
            try {
                const authCodeUrlResponse = await msalInstance.getAuthCodeUrl(req.session.authCodeUrlRequest);
                res.redirect(authCodeUrlResponse);
            } catch (error) {
                next(error);
            }
        }
    
        /**
         * Retrieves oidc metadata from the openid endpoint
         * @returns
         */
        async getAuthorityMetadata() {
            // For external tenant
            //const endpoint = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/v2.0/.well-known/openid-configuration`;
    
            // For workforce tenant
            const endpoint = `${this.config.msalConfig.auth.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: msalConfig,
        redirectUri: REDIRECT_URI,
        postLogoutRedirectUri: POST_LOGOUT_REDIRECT_URI,
    });
    
    module.exports = authProvider;
    
    

    As rotas /signin, /signout e /redirect são definidas no arquivo routes/auth.js, mas você implementa sua lógica na classe auth/AuthProvider.js.

  • O método login para manipular a rota /signin:

    • Ele inicia o processo de início de sessão acionando a primeira etapa do fluxo de código de autenticação.

    • Ele inicializa um aplicativo cliente confidencial instância usando o objeto de configuração MSAL, msalConfig, que você criou anteriormente.

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

      O método getMsalInstance é definido como:

          getMsalInstance(msalConfig) {
              return new msal.ConfidentialClientApplication(msalConfig);
          }
      
    • A primeira etapa do fluxo de código de autenticação gera uma URL de solicitação de código de autorização e, em seguida, redireciona para essa URL para obter o código de autorização. Esta primeira etapa é implementada no método redirectToAuthCodeUrl. Observe como usamos MSALs método getAuthCodeUrl para gerar URL de código de autorização:

      //...
      const authCodeUrlResponse = await msalInstance.getAuthCodeUrl(req.session.authCodeUrlRequest);
      //...
      

      Em seguida, redirecionamos para o próprio URL do código de autorização.

      //...
      res.redirect(authCodeUrlResponse);
      //...
      
  • O método handleRedirect manipula a rota /redirect:

    • Definiu este URL como URI de redirecionamento para a aplicação web no centro de administração do Microsoft Entra anteriormente no Guia de início rápido: iniciar sessão de utilizadores em uma aplicação Web de exemplo.

    • Este ponto de extremidade implementa a segunda fase do processo de fluxo de autenticação por código. Ele usa o código de autorização para solicitar um token de ID usando o método acquireTokenByCode da MSAL.

      //...
      const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body);
      //...
      
    • Depois de receber uma resposta, você pode criar uma sessão Express e armazenar as informações que desejar nela. Você precisa incluir isAuthenticated e defini-lo para true:

      //...        
      req.session.idToken = tokenResponse.idToken;
      req.session.account = tokenResponse.account;
      req.session.isAuthenticated = true;
      //...
      
  • O método logout manipula a rota /signout.

    async logout(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
            */
        const logoutUri = `${this.config.msalConfig.auth.authority}${TENANT_SUBDOMAIN}.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=${this.config.postLogoutRedirectUri}`;
    
        req.session.destroy(() => {
            res.redirect(logoutUri);
        });
    }
    
    • Inicia a solicitação de encerramento de sessão.

    • Quando você deseja sair do aplicativo, não é suficiente encerrar a sessão do usuário. Você deve redirecionar o usuário para o logoutUri. Caso contrário, o usuário poderá se autenticar novamente em seus aplicativos sem reinserir suas credenciais. Se o nome do seu locatário for contoso, o logoutUri será semelhante ao https://contoso.ciamlogin.com/contoso.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=http://localhost:3000.

URI de logout e endpoint de metadados de autoridade para a sua aplicação

O URI de logout do aplicativo e o ponto de extremidade de metadados de autoridade, logoutUri e endpoint, para locatários externos e de força de trabalho têm uma aparência diferente. Construa-os como mostrado abaixo:

//Logout URI for workforce tenant
const logoutUri = `${this.config.msalConfig.auth.authority}/oauth2/v2.0/logout?post_logout_redirect_uri=${this.config.postLogoutRedirectUri}`;

//authority metadata endpoint for workforce tenant
const endpoint = `${this.config.msalConfig.auth.authority}/v2.0/.well-known/openid-configuration`;

Exibir declarações de token de ID

No seu editor de código, abra o arquivo routes/users.js e adicione o seguinte código:

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

// 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 });
    }
);        
module.exports = router;

Se o usuário for autenticado, a rota /id exibirá declarações de token de ID usando a views/id.hbs view. Você adicionou este modo de visualização anteriormente em Construir componentes da interface do usuário do aplicativo.

Para extrair um atributo específico de um token de ID, como nome próprio:

const givenName = req.session.account.idTokenClaims.given_name

Finalize seu aplicativo Web

  1. No editor de códigos, abra app.js arquivo e adicione o código de app.js a ele.

  2. No editor de códigos, abra server.js arquivo e adicione o código de server.js a ele.

  3. No editor de códigos, abra package.json arquivo e atualize a propriedade scripts para:

    "scripts": {
    "start": "node server.js"
    }
    

Executar e testar o aplicativo Web Node/Express.js

Neste ponto, pode testar a sua aplicação web em Node.js.

  1. Use as etapas em Criar um novo utilizador para criar um utilizador de teste no ambiente de trabalho. Se você não tiver acesso ao locatário, peça ao administrador do locatário para criar o usuário para você.

  2. Para iniciar o servidor, execute os seguintes comandos de dentro do diretório do projeto:

    npm start
    
  3. Abra o navegador e vá para http://localhost:3000. Você deve ver a página semelhante à seguinte captura de tela:

    Captura de ecrã do início de sessão numa aplicação web Node.js.

  4. Selecione Iniciar sessão para iniciar o processo de início de sessão. Na primeira vez que iniciar sessão, ser-lhe-á pedido que forneça o seu consentimento para permitir que a aplicação inicie sessão e aceda ao seu perfil, conforme mostrado na captura de ecrã seguinte:

    Captura de tela exibindo a tela de consentimento do usuário

Depois de iniciar sessão com êxito, será redirecionado de volta para a página inicial da aplicação.

Próximo passo