Aktivera autentisering i din egen Node.js webb-API med hjälp av Azure Active Directory B2C

I den här artikeln får du lära dig hur du skapar en webbapp som anropar webb-API:et. Webb-API:et måste skyddas av Azure Active Directory B2C (Azure AD B2C). Om du vill auktorisera åtkomst till ett webb-API kan du hantera begäranden som innehåller en giltig åtkomsttoken som utfärdas av Azure AD B2C.

Förutsättningar

Steg 1: Skapa ett skyddat webb-API

Följ dessa steg för att skapa ditt Node.js webb-API.

Steg 1.1: Skapa projektet

Använd Express för Node.js för att skapa ett webb-API. Gör följande för att skapa ett webb-API:

  1. Skapa en ny mapp med namnet TodoList.
  2. Under mappen TodoList skapar du en fil med namnet index.js.
  3. Kör i ett kommandogränssnitt npm init -y. Det här kommandot skapar en standardfil package.json för ditt Node.js-projekt.
  4. Kör npm install express i kommandogränssnittet. Det här kommandot installerar Express-ramverket.

Steg 1.2: Installera beroenden

Lägg till autentiseringsbiblioteket i ditt webb-API-projekt. Autentiseringsbiblioteket parsar HTTP-autentiseringshuvudet, validerar token och extraherar anspråk. Mer information finns i dokumentationen för biblioteket.

Om du vill lägga till autentiseringsbiblioteket installerar du paketen genom att köra följande kommando:

npm install passport
npm install passport-azure-ad
npm install morgan

Morgan-paketet är ett mellanprogram för HTTP-begärandeloggning för Node.js.

Steg 1.3: Skriva webb-API-serverkoden

index.js Lägg till följande kod i filen:

const express = require('express');
const morgan = require('morgan');
const passport = require('passport');
const config = require('./config.json');
const todolist = require('./todolist');
const cors = require('cors');

//<ms_docref_import_azuread_lib>
const BearerStrategy = require('passport-azure-ad').BearerStrategy;
//</ms_docref_import_azuread_lib>

global.global_todos = [];

//<ms_docref_azureadb2c_options>
const options = {
    identityMetadata: `https://${config.credentials.tenantName}.b2clogin.com/${config.credentials.tenantName}.onmicrosoft.com/${config.policies.policyName}/${config.metadata.version}/${config.metadata.discovery}`,
    clientID: config.credentials.clientID,
    audience: config.credentials.clientID,
    policyName: config.policies.policyName,
    isB2C: config.settings.isB2C,
    validateIssuer: config.settings.validateIssuer,
    loggingLevel: config.settings.loggingLevel,
    passReqToCallback: config.settings.passReqToCallback
}

//</ms_docref_azureadb2c_options>

//<ms_docref_init_azuread_lib>
const bearerStrategy = new BearerStrategy(options, (token, done) => {
        // Send user info using the second argument
        done(null, { }, token);
    }
);
//</ms_docref_init_azuread_lib>
const app = express();

app.use(express.json()); 

//enable CORS (for testing only -remove in production/deployment)
app.use((req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'Authorization, Origin, X-Requested-With, Content-Type, Accept');
    next();
});

app.use(morgan('dev'));

app.use(passport.initialize());

passport.use(bearerStrategy);

// To do list endpoints
app.use('/api/todolist', todolist);

//<ms_docref_protected_api_endpoint>
// API endpoint, one must present a bearer accessToken to access this endpoint
app.get('/hello',
    passport.authenticate('oauth-bearer', {session: false}),
    (req, res) => {
        console.log('Validated claims: ', req.authInfo);
    
          
        // Service relies on the name claim.  
        res.status(200).json({'name': req.authInfo['name']});
    }
);
//</ms_docref_protected_api_endpoint>

//<ms_docref_anonymous_api_endpoint>
// API anonymous endpoint, returns a date to the caller.
app.get('/public', (req, res) => res.send( {'date': new Date() } ));
//</ms_docref_anonymous_api_endpoint>

const port = process.env.PORT || 5000;

app.listen(port, () => {
    console.log('Listening on port ' + port);
});

Anteckna följande kodfragment i index.jsfilen:

  • Importerar passet Microsoft Entra bibliotek

    const BearerStrategy = require('passport-azure-ad').BearerStrategy;
    
  • Anger Azure AD B2C-alternativ

    const options = {
        identityMetadata: `https://${config.credentials.tenantName}.b2clogin.com/${config.credentials.tenantName}.onmicrosoft.com/${config.policies.policyName}/${config.metadata.version}/${config.metadata.discovery}`,
        clientID: config.credentials.clientID,
        audience: config.credentials.clientID,
        policyName: config.policies.policyName,
        isB2C: config.settings.isB2C,
        validateIssuer: config.settings.validateIssuer,
        loggingLevel: config.settings.loggingLevel,
        passReqToCallback: config.settings.passReqToCallback
    }
    
  • Instansiera passport Microsoft Entra-biblioteket med alternativen för Azure AD B2C

    const bearerStrategy = new BearerStrategy(options, (token, done) => {
            // Send user info using the second argument
            done(null, { }, token);
        }
    );
    
  • Den skyddade API-slutpunkten. Den hanterar begäranden som innehåller en giltig Azure AD B2C-utfärdad åtkomsttoken. Den här slutpunkten returnerar värdet för anspråket name i åtkomsttoken.

    // API endpoint, one must present a bearer accessToken to access this endpoint
    app.get('/hello',
        passport.authenticate('oauth-bearer', {session: false}),
        (req, res) => {
            console.log('Validated claims: ', req.authInfo);
        
              
            // Service relies on the name claim.  
            res.status(200).json({'name': req.authInfo['name']});
        }
    );
    
  • Den anonyma API-slutpunkten. Webbappen kan anropa den utan att presentera en åtkomsttoken. Använd den för att felsöka ditt webb-API med anonyma anrop.

    // API anonymous endpoint, returns a date to the caller.
    app.get('/public', (req, res) => res.send( {'date': new Date() } ));
    

Steg 1.4: Konfigurera webb-API:et

Lägg till konfigurationer i en konfigurationsfil. Filen innehåller information om din Azure AD B2C-identitetsprovider. Webb-API-appen använder den här informationen för att verifiera den åtkomsttoken som webbappen skickar som en ägartoken.

  1. Under projektrotmappen skapar du en config.json fil och lägger sedan till följande JSON-objekt i den:

    {
        "credentials": {
            "tenantName": "fabrikamb2c",
            "clientID": "93733604-cc77-4a3c-a604-87084dd55348"
        },
        "policies": {
            "policyName": "B2C_1_susi"
        },
        "resource": {
            "scope": ["tasks.read"]
        },
        "metadata": {
            "authority": "login.microsoftonline.com",
            "discovery": ".well-known/openid-configuration",
            "version": "v2.0"
        },
        "settings": {
            "isB2C": true,
            "validateIssuer": true,
            "passReqToCallback": false,
            "loggingLevel": "info"
        }
    }
    
  2. config.json Uppdatera följande egenskaper i filen:

Avsnitt Tangent Värde
autentiseringsuppgifter tenantName Den första delen av din Azure AD B2C-klientnamn (till exempel fabrikamb2c).
autentiseringsuppgifter clientID Webb-API-program-ID. Information om hur du hämtar ditt programregistrerings-ID för webb-API:et finns i Krav.
policies policyName Användarflödena eller den anpassade principen. Information om hur du hämtar ditt användarflöde eller din princip finns i Krav.
resource omfång Omfången för din webb-API-programregistrering, till exempel [tasks.read]. Information om hur du hämtar ditt webb-API-omfång finns i Krav.

Steg 2: Skapa webbnodwebbappen

Följ de här stegen för att skapa Node-webbappen. Den här webbappen autentiserar en användare för att hämta en åtkomsttoken som används för att anropa nodwebb-API:et som du skapade i steg 1:

Steg 2.1: Skapa nodprojektet

Skapa en mapp för att lagra nodprogrammet, till exempel call-protected-api.

  1. I terminalen ändrar du katalogen till nodappmappen, till exempel cd call-protected-api, och kör npm init -y. Det här kommandot skapar standardfilen package.json för Node.js-projektet.

  2. Kör npm install express i terminalen. Det här kommandot installerar Express-ramverket.

  3. Skapa fler mappar och filer för att uppnå följande projektstruktur:

    call-protected-api/
    ├── index.js
    └── package.json
    └── .env
    └── views/
        └── layouts/
            └── main.hbs
        └── signin.hbs
        └── api.hbs
    

    Mappen views innehåller styrfiler för webbappens användargränssnitt.

Steg 2.2: Installera beroendena

Installera paketen , express-handlebars, express-sessionoch @azure/msal-node i terminalen dotenvgenom att köra följande kommandon:

npm install dotenv
npm install express-handlebars
npm install express
npm install axios
npm install express-session
npm install @azure/msal-node

Steg 2.3: Skapa gränssnittskomponenter för webbappar

  1. main.hbs Lägg till följande kod i filen:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
        <title>Azure AD B2C | Enable authenticate on web API using MSAL for B2C</title>
    
        <!-- adding Bootstrap 4 for UI components  -->
        <!-- CSS only -->
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
        <link rel="SHORTCUT ICON" href="https://c.s-microsoft.com/favicon.ico?v2" type="image/x-icon">
      </head>
      <body>
        <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
          <a class="navbar-brand" href="/">Microsoft Identity Platform</a>
            {{#if showSignInButton}}
                <div class="ml-auto">
                    <a type="button" id="SignIn" class="btn btn-success" href="/signin" aria-haspopup="true" aria-expanded="false">
                        Sign in to call PROTECTED API
                    </a>
                    <a type="button" id="SignIn" class="btn btn-warning" href="/api" aria-haspopup="true" aria-expanded="false">
                        Or call the ANONYMOUS API 
                     </a>
                </div>
            {{else}}
                    <p class="navbar-brand d-flex ms-auto">Hi {{givenName}}</p>
                    <a class="navbar-brand d-flex ms-auto" href="/signout">Sign out</a>
            {{/if}}
        </nav>
        <br>
        <h5 class="card-header text-center">MSAL Node Confidential Client application with Auth Code Flow</h5>
        <br>
        <div class="row" style="margin:auto" >
          {{{body}}}
        </div>
        <br>
        <br>
      </body>
    </html>
    

    Filen main.hbs finns i layout mappen och den bör innehålla all HTML-kod som krävs i hela programmet. Det implementerar användargränssnitt som skapats med Bootstrap 5 CSS Framework. Alla användargränssnitt som ändras från sida till sida, till exempel signin.hbs, placeras i platshållaren som visas som {{{body}}}.

  2. signin.hbs Lägg till följande kod i filen:

    <div class="col-md-3" style="margin:auto">
      <div class="card text-center">
        <div class="card-body">
          {{#if showSignInButton}}
    
          {{else}}
               <h5 class="card-title">You have signed in</h5>
              <a type="button" id="Call-api" class="btn btn-success" href="/api" aria-haspopup="true" aria-expanded="false">
                  Call the PROTECTED API
              </a>
          {{/if}}
        </div>
        </div>
      </div>
    </div>
    
  3. api.hbs Lägg till följande kod i filen:

    <div class="col-md-3" style="margin:auto">
      <div class="card text-center bg-{{bg_color}}">
        <div class="card-body">
    
              <h5 class="card-title">{{data}}</h5>
    
        </div>
      </div>
    </div>
    

    Den här sidan visar svaret från API:et. Med bg-{{bg_color}} klassattributet i Bootstrap-kortet kan användargränssnittet visa en annan bakgrundsfärg för de olika API-slutpunkterna.

Steg 2.4: Slutför webbprogramserverkoden

  1. .env I filen lägger du till följande kod, som innehåller http-port för servern, information om appregistrering och inloggning och registrering av användarflöde/principinformation:

    SERVER_PORT=3000
    #web apps client ID
    APP_CLIENT_ID=<You app client ID here>
    #session secret
    SESSION_SECRET=sessionSecretHere
    #web app client secret
    APP_CLIENT_SECRET=<Your app client secret here>
    #tenant name
    TENANT_NAME=<your-tenant-name>
    #B2C sign up and sign in user flow/policy name and authority
    SIGN_UP_SIGN_IN_POLICY_AUTHORITY=https://<your-tenant-name>.b2clogin.com/<your-tenant-name>.onmicrosoft.com/<sign-in-sign-up-user-flow-name>
    AUTHORITY_DOMAIN=https://<your-tenant-name>.b2clogin.com
    #client redorect url
    APP_REDIRECT_URI=http://localhost:3000/redirect
    LOGOUT_ENDPOINT=https://<your-tenant-name>.b2clogin.com/<your-tenant-name>.onmicrosoft.com/<sign-in-sign-up-user-flow-name>/oauth2/v2.0/logout?post_logout_redirect_uri=http://localhost:3000
    

    Ändra värdena i filerna enligt beskrivningen .env i Konfigurera exempelwebbappen

  2. index.js Lägg till följande kod i filen:

    /*
     * Copyright (c) Microsoft Corporation. All rights reserved.
     * Licensed under the MIT License.
     */
    require('dotenv').config();
    const express = require('express');
    const session = require('express-session');
    const {engine}  = require('express-handlebars');
    const msal = require('@azure/msal-node');
    //Use axios to make http calls 
    const axios = require('axios');
    
    //<ms_docref_configure_msal>
    /**
     * Confidential Client Application Configuration
     */
     const confidentialClientConfig = {
        auth: {
            clientId: process.env.APP_CLIENT_ID, 
            authority: process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY, 
            clientSecret: process.env.APP_CLIENT_SECRET,
            knownAuthorities: [process.env.AUTHORITY_DOMAIN], //This must be an array
            redirectUri: process.env.APP_REDIRECT_URI,
            validateAuthority: false
        },
        system: {
            loggerOptions: {
                loggerCallback(loglevel, message, containsPii) {
                    console.log(message);
                },
                piiLoggingEnabled: false,
                logLevel: msal.LogLevel.Verbose,
            }
        }
    };
    
    // Initialize MSAL Node
    const confidentialClientApplication = new msal.ConfidentialClientApplication(confidentialClientConfig);
    //</ms_docref_configure_msal>
    // Current web API coordinates were pre-registered in a B2C tenant.
    
    //<ms_docref_api_config>
    const apiConfig = {
        webApiScopes: [`https://${process.env.TENANT_NAME}.onmicrosoft.com/tasks-api/tasks.read`],
        anonymousUri: 'http://localhost:5000/public',
        protectedUri: 'http://localhost:5000/hello'
    };
    //</ms_docref_api_config>
    
    /**
     * The MSAL.js library allows you to pass your custom state as state parameter in the Request object
     * By default, MSAL.js passes a randomly generated unique state parameter value in the authentication requests.
     * 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.
     * For more information, visit: https://docs.microsoft.com/azure/active-directory/develop/msal-js-pass-custom-state-authentication-request
     */
    const APP_STATES = {
        LOGIN: 'login',
        CALL_API:'call_api'   
    }
    
    
    /** 
     * Request Configuration
     * We manipulate these two request objects below 
     * to acquire a token with the appropriate claims.
     */
     const authCodeRequest = {
        redirectUri: confidentialClientConfig.auth.redirectUri,
    };
    
    const tokenRequest = {
        redirectUri: confidentialClientConfig.auth.redirectUri,
    };
    
    
    /**
     * Using express-session middleware. Be sure to familiarize yourself with available options
     * and set them as desired. Visit: https://www.npmjs.com/package/express-session
     */
     const sessionConfig = {
        secret: process.env.SESSION_SECRET,
        resave: false,
        saveUninitialized: false,
        cookie: {
            secure: false, // set this to true on production
        }
    }
    //Create an express instance
    const app = express();
    
    //Set handlebars as your view engine
    app.engine('.hbs', engine({extname: '.hbs'}));
    app.set('view engine', '.hbs');
    app.set("views", "./views");
    
    app.use(session(sessionConfig));
    
    /**
     * This method is used to generate an auth code request
     * @param {string} authority: the authority to request the auth code from 
     * @param {array} scopes: scopes to request the auth code for 
     * @param {string} state: state of the application, tag a request
     * @param {Object} res: express middleware response object
     */
    
     const getAuthCode = (authority, scopes, state, res) => {
        // prepare the request
        console.log("Fetching Authorization code")
        authCodeRequest.authority = authority;
        authCodeRequest.scopes = scopes;
        authCodeRequest.state = state;
    
        //Each time you fetch Authorization code, update the authority in the tokenRequest configuration
        tokenRequest.authority = authority;
    
        // request an authorization code to exchange for a token
        return confidentialClientApplication.getAuthCodeUrl(authCodeRequest)
            .then((response) => {
                console.log("\nAuthCodeURL: \n" + response);
                //redirect to the auth code URL/send code to 
                res.redirect(response);
            })
            .catch((error) => {
                res.status(500).send(error);
            });
    }
    
    app.get('/', (req, res) => {
        res.render('signin', { showSignInButton: true });
    });
    
    
    
    app.get('/signin',(req, res)=>{ 
            //Initiate a Auth Code Flow >> for sign in
            //Pass the api scopes as well so that you received both the IdToken and accessToken
            getAuthCode(process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY,apiConfig.webApiScopes, APP_STATES.LOGIN, res);
    });
    
    
    app.get('/redirect',(req, res)=>{    
        
        if (req.query.state === APP_STATES.LOGIN) {
            // prepare the request for calling the web API
            tokenRequest.authority = process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY;
            tokenRequest.scopes = apiConfig.webApiScopes;
            tokenRequest.code = req.query.code;
            confidentialClientApplication.acquireTokenByCode(tokenRequest)
            .then((response) => {
                req.session.accessToken = response.accessToken;
                req.session.givenName = response.idTokenClaims.given_name;
                console.log('\nAccessToken:' + req.session.accessToken);
                res.render('signin', {showSignInButton: false, givenName: response.idTokenClaims.given_name});
            }).catch((error) => {
                console.log(error);
                res.status(500).send(error);
            });
        }else{
            res.status(500).send('We do not recognize this response!');
        }
    });
    
    //<ms_docref_api_express_route>
    app.get('/api', async (req, res) => {
        if(!req.session.accessToken){
            //User is not logged in and so they can only call the anonymous API
            try {
                const response = await axios.get(apiConfig.anonymousUri);
                console.log('API response' + response.data); 
                res.render('api',{data: JSON.stringify(response.data), showSignInButton: true, bg_color:'warning'});
            } catch (error) {
                console.error(error);
                res.status(500).send(error);
            }         
        }else{
            //Users have the accessToken because they signed in and the accessToken is still in the session
            console.log('\nAccessToken:' + req.session.accessToken);
            let accessToken = req.session.accessToken;
            const options = {
                headers: {
                    //accessToken used as bearer token to call a protected API
                    Authorization: `Bearer ${accessToken}`
                }
            };
    
            try {
                const response = await axios.get(apiConfig.protectedUri, options);
                console.log('API response' + response.data); 
                res.render('api',{data: JSON.stringify(response.data), showSignInButton: false, bg_color:'success', givenName: req.session.givenName});
            } catch (error) {
                console.error(error);
                res.status(500).send(error);
            }
        }     
    });
    
    //</ms_docref_api_express_route>
    
    /**
     * Sign out end point
    */
    app.get('/signout',async (req, res)=>{    
        logoutUri = process.env.LOGOUT_ENDPOINT;
        req.session.destroy(() => {
            res.redirect(logoutUri);
        });
    });
    app.listen(process.env.SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port !` + process.env.SERVER_PORT));
    

    Koden i index.js filen består av globala variabler och expressvägar.

    Globala variabler:

    • confidentialClientConfig: MSAL-konfigurationsobjektet som används för att skapa det konfidentiella klientprogramobjektet.

      /**
       * Confidential Client Application Configuration
       */
       const confidentialClientConfig = {
          auth: {
              clientId: process.env.APP_CLIENT_ID, 
              authority: process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY, 
              clientSecret: process.env.APP_CLIENT_SECRET,
              knownAuthorities: [process.env.AUTHORITY_DOMAIN], //This must be an array
              redirectUri: process.env.APP_REDIRECT_URI,
              validateAuthority: false
          },
          system: {
              loggerOptions: {
                  loggerCallback(loglevel, message, containsPii) {
                      console.log(message);
                  },
                  piiLoggingEnabled: false,
                  logLevel: msal.LogLevel.Verbose,
              }
          }
      };
      
      // Initialize MSAL Node
      const confidentialClientApplication = new msal.ConfidentialClientApplication(confidentialClientConfig);
      
    • apiConfig: Innehåller webApiScopes egenskapen (dess värde måste vara en matris), vilket är de omfång som konfigurerats i webb-API:et och beviljats till webbappen. Den har också URI:er till webb-API:et som ska anropas, det vill: anonymousUri och protectedUri.

      const apiConfig = {
          webApiScopes: [`https://${process.env.TENANT_NAME}.onmicrosoft.com/tasks-api/tasks.read`],
          anonymousUri: 'http://localhost:5000/public',
          protectedUri: 'http://localhost:5000/hello'
      };
      
    • APP_STATES: Ett värde som ingår i begäran som också returneras i tokensvaret. Används för att skilja mellan svar som tas emot från Azure AD B2C.

    • authCodeRequest: Konfigurationsobjektet som används för att hämta auktoriseringskod.

    • tokenRequest: Konfigurationsobjektet som används för att hämta en token med auktoriseringskod.

    • sessionConfig: Konfigurationsobjektet för expresssession.

    • getAuthCode: En metod som skapar URL:en för auktoriseringsbegäran så att användaren kan ange autentiseringsuppgifter och godkänna programmet. Den använder getAuthCodeUrl metoden, som definieras i klassen ConfidentialClientApplication .

    Expressvägar:

    • /:
      • Det är posten i webbappen och återger signin sidan.
    • /signin:
      • Loggar in användaren.
      • Anropar getAuthCode() metoden och skickar authority för Inloggning och registrera användarflöde/princip, APP_STATES.LOGIN, och apiConfig.webApiScopes till den.
      • Det gör att slutanvändaren uppmanas att ange sina inloggningar, eller om användaren inte har något konto kan de registrera sig.
      • Det slutliga svaret från den här slutpunkten innehåller en auktoriseringskod från B2C som publiceras tillbaka till /redirect slutpunkten.
    • /redirect:
      • Det är slutpunkten som anges som omdirigerings-URI för webbappen i Azure Portal.
      • Den använder state frågeparametern i Azure AD B2C:s svar för att skilja mellan begäranden som görs från webbappen.
      • Om apptillståndet är APP_STATES.LOGINanvänds den auktoriseringskod som hämtas för att hämta en token med hjälp av acquireTokenByCode() metoden . När du begär en token med hjälp av acquireTokenByCode metoden använder du samma omfång som används när du hämtar auktoriseringskoden. Den förvärvade token innehåller en accessToken, idTokenoch idTokenClaims. När du har hämtat accessTokenplacerar du den i en session för senare användning för att anropa webb-API:et.
    • /api:
      • Anropar webb-API:et.
      • accessToken Om inte är i sessionen anropar du den anonyma API-slutpunkten (http://localhost:5000/public), annars anropar du den skyddade API-slutpunkten (http://localhost:5000/hello).
    • /signout:
      • Loggar ut användaren.
      • rensar webbappsessionen och gör ett http-anrop till slutpunkten för Azure AD B2C-utloggning.

Steg 3: Kör webbappen och API:et

Följ stegen i Kör webbappen och API:et för att testa webbappen och webb-API:et.

Nästa steg