다음을 통해 공유


자습서: Microsoft ID 플랫폼을 사용하여 노드/Express.js 웹앱에 로그인 추가

적용 대상: 이 내용은 워크포스 테넌트에 적용됩니다. 흰색 체크 표시가 있는 녹색 원으로 표시됩니다.이 내용은 외부 테넌트에 적용됩니다. 흰색 체크 표시가 있는 녹색 원으로 표시됩니다. 외부 테넌트(자세한 정보)

이 자습서에서는 Node/Express 웹앱에 로그인 및 로그아웃 논리를 추가합니다. 이 코드를 사용하면 외부 테넌트 또는 직원 테넌트에서 고객을 대상으로 하는 앱에 사용자를 로그인할 수 있습니다.

이 자습서는 3부로 구성된 자습서 시리즈의 2부입니다.

이 자습서에서는 다음을 수행합니다.

  • 로그인 및 로그아웃 논리 추가
  • ID 토큰 클레임 보기
  • 앱을 실행하고 로그인 및 로그아웃 환경을 테스트합니다.

필수 구성 요소

MSAL 구성 개체 만들기

코드 편집기에서 authConfig.js 파일을 연 다음, 다음 코드를 추가합니다.

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
};

msalConfig 개체에는 인증 흐름의 동작을 사용자 지정하는 데 사용하는 구성 옵션 집합이 포함되어 있습니다.

authConfig.js 파일에서 다음을 대체합니다.

  • 이전에 등록한 앱의 애플리케이션(클라이언트) ID로 Enter_the_Application_Id_Here을 교체하세요.

  • Enter_the_Tenant_Subdomain_Here 외부 디렉터리(테넌트) 하위 도메인으로 바꿉니다. 예를 들어 테넌트 주 도메인이 contoso.onmicrosoft.com경우 contoso사용합니다. 테넌트 이름이 없는 경우 테넌트 세부 정보를읽는 방법을 알아봅니다. 이 값은 외부 테넌트경우에만 필요합니다.

  • 이전에 복사한 앱 비밀 값으로 Enter_the_Client_Secret_Here를 교체하십시오.

  • 앱에서 호출할 Microsoft Graph API 클라우드 인스턴스와 함께 Enter_the_Graph_Endpoint_Here을 사용하십시오. https://graph.microsoft.com/ 값을 사용하되, 끝에 슬래시를 포함하세요.

.env 파일을 사용하여 구성 정보를 저장하는 경우:

  1. 코드 편집기에서 .env 파일을 연 다음 다음 코드를 추가합니다.

        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. 자리 표시자를 교체하십시오.

    1. 앞에서 설명한 대로 Enter_the_Application_Id_Here, Enter_the_Tenant_Subdomain_HereEnter_the_Client_Secret_Here.
    2. 애플리케이션이 등록된 Azure 클라우드 인스턴스에 Enter_the_Cloud_Instance_Id_Here가 존재합니다. https://login.microsoftonline.com/을 해당 값으로 사용하십시오(뒤에 슬래시 포함). 이 값은 직원 테넌트경우에만 필요합니다.
    3. Enter_the_Tenant_ID_here 직원 테넌트 ID 또는 주 도메인과 같은, 예를 들어 aaaabbbb-0000-cccc-1111-dddd2222eeee 또는 contoso.microsoft.com. 이 값은 직원 테넌트경우에만 필요합니다.

msalConfig 파일에서 REDIRECT_URI, TENANT_SUBDOMAIN, GRAPH_ME_ENDPOINT, POST_LOGOUT_REDIRECT_URI 변수를 내보내 다른 파일에서 액세스할 수 있도록 합니다.

앱에 대한 권한 부여 URL

외부 사용자 및 직원 사용자의 애플리케이션 권한은 다르게 나타납니다. 아래와 같이 빌드합니다.

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

사용자 지정 URL 도메인 사용(선택 사항)

사용자 지정 URL 도메인은 직원 테넌트에서 지원되지 않습니다.

Express 경로 추가

Express 경로는 로그인, 로그아웃 및 ID 토큰 클레임 보기와 같은 작업을 실행할 수 있는 엔드포인트를 제공합니다.

앱 진입점

코드 편집기에서 경로/index.js 파일을 연 다음 다음 코드를 추가합니다.

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;

/ 경로는 애플리케이션에 대한 진입점입니다. 빌드 앱 UI 구성 요소에서 앞에서 만든 views/index.hbs 보기를 렌더링합니다. isAuthenticated 뷰에 표시되는 내용을 결정하는 부울 변수입니다.

로그인 및 로그아웃

  1. 코드 편집기에서 경로/auth.js 파일을 연 다음 다음 코드를 추가합니다.

    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. 코드 편집기에서 컨트롤러/authController.js 파일을 연 다음 다음 코드를 추가합니다.

    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. 코드 편집기에서 인증/AuthProvider.js 파일을 연 다음 다음 코드를 추가합니다.

    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;
    
    

    /signin, /signout/redirect 경로는 경로/auth.js 파일에 정의되지만 인증/AuthProvider.js 클래스에서 해당 논리를 구현합니다.

  • login 메서드는 /signin 경로를 처리합니다.

    • 인증 코드 흐름의 첫 번째 레그를 트리거하여 로그인 흐름을 시작합니다.

    • 앞에서 만든 MSAL 구성 개체 사용하여 msalConfig 인스턴스를 초기화합니다.

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

      getMsalInstance 메서드는 다음과 같이 정의됩니다.

          getMsalInstance(msalConfig) {
              return new msal.ConfidentialClientApplication(msalConfig);
          }
      
    • 인증 코드 흐름의 첫 번째 레그는 권한 부여 코드 요청 URL을 생성한 다음 해당 URL로 리디렉션하여 권한 부여 코드를 가져옵니다. 이 첫 번째 다리는 redirectToAuthCodeUrl 메서드에서 구현됩니다. MSLL getAuthCodeUrl 메서드를 사용하여 권한 부여 코드 URL을 생성하는 방법을 확인합니다.

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

      그런 다음 권한 부여 코드 URL 자체로 리디렉션합니다.

      //...
      res.redirect(authCodeUrlResponse);
      //...
      
  • handleRedirect 메서드는 /redirect 경로를 처리합니다.

    • 이 URL은 빠른 시작: 샘플 웹앱에서 사용자 로그인의 앞부분에서 Microsoft Entra 관리 센터의 웹 앱에 대한 리디렉션 URI로 설정했습니다.

    • 이 엔드포인트는 인증 코드 흐름에서 사용하는 두 번째 레그를 구현합니다. 권한 부여 코드를 사용하여 MSAL의 acquireTokenByCode 메서드를 사용하여 ID 토큰을 요청합니다.

      //...
      const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body);
      //...
      
    • 응답을 받은 후 Express 세션을 만들고 원하는 정보를 저장할 수 있습니다. isAuthenticated을 포함하고 true로 설정해야 합니다.

      //...        
      req.session.idToken = tokenResponse.idToken;
      req.session.account = tokenResponse.account;
      req.session.isAuthenticated = true;
      //...
      
  • logout 메서드는 /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);
        });
    }
    
    • 로그아웃 요청을 시작합니다.

    • 애플리케이션에서 사용자를 로그아웃하려는 경우 사용자의 세션을 종료하는 것만으로는 충분하지 않습니다. logoutUri로 사용자를 리디렉션해야 합니다. 그렇지 않으면 사용자가 자격 증명을 다시 입력하지 않고 애플리케이션에 다시 인증할 수 있습니다. 테넌트의 이름이 contoso인 경우에, logoutUrihttps://contoso.ciamlogin.com/contoso.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=http://localhost:3000와 비슷하게 보입니다.

앱에 대한 로그아웃 URI 및 권한 메타데이터 엔드포인트

외부 및 인력 테넌트의 경우, 앱의 로그아웃 URI(logoutUri)와 권한 메타데이터 엔드포인트(endpoint)가 다르게 보입니다. 아래와 같이 빌드합니다.

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

ID 토큰 클레임 보기

코드 편집기에서 경로/users.js 파일을 연 다음 다음 코드를 추가합니다.

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;

사용자가 인증되면 /id 경로는 views/id.hbs 보기를 사용하여 ID 토큰 클레임을 표시합니다. 이 보기는 이전에 빌드 앱 UI 구성 요소에서 추가되었습니다.

지정된 이름 같은 특정 ID 토큰 클레임을 추출하려면 다음을 수행합니다.

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

웹앱 완료

  1. 코드 편집기에서 app.js 파일을 연 다음 app.js 코드를 추가합니다.

  2. 코드 편집기에서 server.js 파일을 연 다음 server.js 코드를 추가합니다.

  3. 코드 편집기에서 package.json 파일을 연 다음 scripts 속성을 다음으로 업데이트합니다.

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

노드/Express.js 웹앱 실행 및 테스트

이 시점에서 노드 웹앱을 테스트할 수 있습니다.

  1. 새 사용자 만들기의 단계를 사용하여 워크포스 테넌트에서 테스트 사용자를 만든다. 테넌트에 액세스할 수 없는 경우 테넌트 관리자에게 사용자를 만들어 달라고 요청합니다.

  2. 서버를 시작하려면 프로젝트 디렉터리 내에서 다음 명령을 실행합니다.

    npm start
    
  3. 브라우저를 연 후, 그런 다음 http://localhost:3000으로 이동하세요. 다음 스크린샷과 유사한 페이지가 표시됩니다.

    노드 웹앱에 로그인하는 스크린샷.

  4. 로그인 선택하여 로그인 프로세스를 시작합니다. 처음 로그인할 때 다음 스크린샷과 같이 애플리케이션에서 로그인하고 프로필에 액세스할 수 있도록 동의를 제공하라는 메시지가 표시됩니다.

    사용자 동의 화면Screenshot displaying user consent screenScreenshot displaying user consent screenScreenshot displaying user consent screen표시하는스크린샷

성공적으로 로그인하면 애플리케이션 홈페이지로 다시 리디렉션됩니다.

다음 단계