Självstudie: Logga in användare och hämta en token för Microsoft Graph i en Node.js & Express-webbapp

I den här självstudien skapar du en webbapp som loggar in användare och hämtar åtkomsttoken för att anropa Microsoft Graph. Webbappen som du skapar använder Microsoft Authentication Library (MSAL) för Node.

Följ stegen i den här självstudien för att:

  • Registrera programmet i Azure-portalen
  • Skapa ett Express-webbappsprojekt
  • Installera paketen för autentiseringsbiblioteket
  • Lägga till appregistreringsinformation
  • Lägga till kod för användarinloggning
  • Testa appen

Mer information finns i exempelkoden som visar hur du använder MSAL Node för att logga in, logga ut och hämta en åtkomsttoken för en skyddad resurs, till exempel Microsoft Graph.

Förutsättningar

Registrera programmet

Slutför först stegen i Registrera ett program med Microsofts identitetsplattform för att registrera din app.

Använd följande inställningar för din appregistrering:

  • Namn: ExpressWebApp (föreslås)
  • Kontotyper som stöds: Endast konton i den här organisationskatalogen
  • Plattformstyp: Webb
  • Omdirigerings-URI: http://localhost:3000/auth/redirect
  • Klienthemlighet: ********* (registrera det här värdet för användning i ett senare steg – det visas bara en gång)

Skapa projektet

Använd verktyget Express-programgenerator för att skapa ett programskelett.

  1. Installera först expressgeneratorpaketet:
    npm install -g express-generator
  1. Skapa sedan ett programskelett på följande sätt:
    express --view=hbs /ExpressWebApp && cd /ExpressWebApp
    npm install

Nu har du en enkel Express-webbapp. Projektets fil- och mappstruktur bör se ut ungefär som i följande mappstruktur:

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

Installera autentiseringsbiblioteket

Leta upp roten för projektkatalogen i en terminal och installera MSAL Node-paketet via npm.

    npm install --save @azure/msal-node

Installera andra beroenden

Webbappexemplet i den här självstudien använder expresssessionspaketet för sessionshantering, dotenv-paket för att läsa miljöparametrar under utveckling och axios för att göra nätverksanrop till Microsoft Graph API. Installera dessa via npm:

    npm install --save express-session dotenv axios

Lägga till appregistreringsinformation

  1. Skapa en .env.dev-fil i roten för projektmappen. Lägg sedan till följande kod:
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"

Fyll i den här informationen med de värden som du får från Azure-appregistreringsportalen:

  • Enter_the_Cloud_Instance_Id_Here: Azure-molninstansen där ditt program är registrerat.
    • För det huvudsakliga (eller globala) Azure-molnet anger du https://login.microsoftonline.com/ (inkludera det avslutande snedstrecket).
    • För nationella moln (till exempel Kina) kan du hitta lämpliga värden i nationella moln.
  • Enter_the_Tenant_Info_here bör vara en av följande parametrar:
    • Om ditt program stöder konton i den här organisationskatalogen ersätter du det här värdet med klientorganisations-ID:t eller klientorganisationens namn. Exempel: contoso.microsoft.com
    • Om ditt program stöder konton i en organisationskatalog ersätter du det här värdet med organizations.
    • Om ditt program stöder konton i en organisationskatalog och personliga Microsoft-konton ersätter du det här värdet med common.
    • Om du bara vill begränsa stödet till personliga Microsoft-konton ersätter du det här värdet med consumers.
  • Enter_the_Application_Id_Here: Program-ID :t (klient) för det program som du registrerade.
  • Enter_the_Client_secret: Ersätt det här värdet med klienthemligheten som du skapade tidigare. Om du vill generera en ny nyckel använder du Certifikat och hemligheter i appregistreringsinställningarna i Azure-portalen.

Varning

All klartexthemlighet i källkoden utgör en ökad säkerhetsrisk. Den här artikeln använder endast en klartextklienthemlighet för enkelhetens skull. Använd certifikatautentiseringsuppgifter i stället för klienthemligheter i dina konfidentiella klientprogram, särskilt de appar som du tänker distribuera till produktion.

  • Enter_the_Graph_Endpoint_Here: Molninstansen för Microsoft Graph API som appen anropar. För den huvudsakliga (globala) Microsoft Graph API-tjänsten anger du https://graph.microsoft.com/ (inkludera det avslutande snedstrecket).
  • Enter_the_Express_Session_Secret_Here hemligheten som används för att signera Express-sessionscookien. Välj en slumpmässig sträng med tecken som du vill ersätta strängen med, till exempel din klienthemlighet.
  1. Skapa sedan en fil med namnet authConfig.js i roten av projektet för att läsa i dessa parametrar. När du har skapat den lägger du till följande kod där:
/*
 * 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
};

Lägga till kod för användarinloggning och tokenförvärv

  1. Skapa en ny mapp med namnet auth och lägg till en ny fil med namnet AuthProvider.js under den. Detta kommer att innehålla klassen AuthProvider , som kapslar in nödvändig autentiseringslogik med hjälp av MSAL Node. Lägg till följande kod där:
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. Skapa sedan en ny fil med namnet auth.js under mappen routes och lägg till följande kod där:
/*
 * 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. Uppdatera den index.js vägen genom att ersätta den befintliga koden med följande kodfragment:
/*
 * 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. Uppdatera slutligen den users.js vägen genom att ersätta den befintliga koden med följande kodfragment:
/*
 * 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;

Lägg till kod för att anropa Microsoft Graph API

Skapa en fil med namnet fetch.js i projektets rot och lägg till följande kod:

/*
 * 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;

Lägga till vyer för att visa data

  1. Uppdatera filen index.hbs i mappen views genom att ersätta den befintliga koden med följande:
<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. Fortfarande i samma mapp skapar du en annan fil med namnet id.hbs för att visa innehållet i användarens ID-token:
<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. Skapa slutligen en annan fil med namnet profile.hbs för att visa resultatet av anropet till 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>

Registrera routrar och lägg till tillståndshantering

I den app.js filen i roten i projektmappen registrerar du de vägar som du skapade tidigare och lägger till sessionsstöd för spårning av autentiseringstillstånd med hjälp av expresssessionspaketet . Ersätt den befintliga koden där med följande kodfragment:

/*
 * 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;

Testa inloggningen och anropa Microsoft Graph

Du har slutfört skapandet av programmet och är nu redo att testa appens funktioner.

  1. Starta Node.js-konsolappen genom att köra följande kommando från roten i projektmappen:
   npm start
  1. Öppna en webbläsare och gå till http://localhost:3000. Du bör se en välkomstsida:

Web app welcome page displaying

  1. Välj Länken Logga in . Du bör se inloggningsskärmen för Microsoft Entra:

Microsoft Entra sign-in screen displaying

  1. När du har angett dina autentiseringsuppgifter bör du se en medgivandeskärm där du uppmanas att godkänna appens behörigheter.

Microsoft Entra consent screen displaying

  1. När du har samtyckt bör du omdirigeras tillbaka till programmets startsida.

Web app welcome page after sign-in displaying

  1. Välj länken Visa ID-token för att visa innehållet i den inloggade användarens ID-token.

User ID token screen displaying

  1. Gå tillbaka till startsidan och välj länken Hämta en åtkomsttoken och anropa länken Microsoft Graph API . När du har det bör du se svaret från Microsoft Graph/me-slutpunkten för den inloggade användaren.

Graph call screen displaying

  1. Gå tillbaka till startsidan och välj länken Logga ut . Du bör se utloggningsskärmen för Microsoft Entra.

Microsoft Entra sign-out screen displaying

Så här fungerar programmet

I den här självstudien instansierade du ett MSAL Node ConfidentialClientApplication-objekt genom att skicka det ett konfigurationsobjekt (msalConfig) som innehåller parametrar som hämtats från din Microsoft Entra-appregistrering på Azure-portalen. Webbappen som du skapade använder OpenID Anslut-protokollet för att logga in användare och OAuth 2.0-auktoriseringskodflödet för att hämta åtkomsttoken.

Nästa steg

Om du vill fördjupa dig i Node.js & Express-webbprogramutveckling på Microsofts identitetsplattform kan du läsa vår serie med flerdelade scenarion: