적용 대상: 
외부 테넌트(자세한 정보)
이 자습서에서는 Node/Express 웹앱에 로그인 및 로그아웃 논리를 추가합니다. 이 코드를 사용하면 외부 테넌트 또는 직원 테넌트에서 고객을 대상으로 하는 앱에 사용자를 로그인할 수 있습니다.
이 자습서는 3부로 구성된 자습서 시리즈의 2부입니다.
이 자습서에서는 다음을 수행합니다.
- 로그인 및 로그아웃 논리 추가
- ID 토큰 클레임 보기
- 앱을 실행하고 로그인 및 로그아웃 환경을 테스트합니다.
필수 구성 요소
- 자습서의 단계를 완료합니다. Microsoft ID 플랫폼사용하여 사용자를 로그인하도록 Node.js 웹앱을 설정합니다.
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 파일을 사용하여 구성 정보를 저장하는 경우:
코드 편집기에서 .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자리 표시자를 교체하십시오.
- 앞에서 설명한 대로
Enter_the_Application_Id_Here,Enter_the_Tenant_Subdomain_Here및Enter_the_Client_Secret_Here. - 애플리케이션이 등록된 Azure 클라우드 인스턴스에
Enter_the_Cloud_Instance_Id_Here가 존재합니다.https://login.microsoftonline.com/을 해당 값으로 사용하십시오(뒤에 슬래시 포함). 이 값은 직원 테넌트경우에만 필요합니다. -
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 도메인 사용(선택 사항)
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 뷰에 표시되는 내용을 결정하는 부울 변수입니다.
로그인 및 로그아웃
코드 편집기에서 경로/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;코드 편집기에서 컨트롤러/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); };코드 편집기에서 인증/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인 경우에, logoutUri는
https://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
웹앱 완료
코드 편집기에서 app.js 파일을 연 다음 app.js 코드를 추가합니다.
코드 편집기에서 server.js 파일을 연 다음 server.js 코드를 추가합니다.
코드 편집기에서 package.json 파일을 연 다음
scripts속성을 다음으로 업데이트합니다."scripts": { "start": "node server.js" }
노드/Express.js 웹앱 실행 및 테스트
이 시점에서 노드 웹앱을 테스트할 수 있습니다.
새 사용자 만들기의 단계를 사용하여 워크포스 테넌트에서 테스트 사용자를 만든다. 테넌트에 액세스할 수 없는 경우 테넌트 관리자에게 사용자를 만들어 달라고 요청합니다.
서버를 시작하려면 프로젝트 디렉터리 내에서 다음 명령을 실행합니다.
npm start브라우저를 연 후, 그런 다음
http://localhost:3000으로 이동하세요. 다음 스크린샷과 유사한 페이지가 표시됩니다.
로그인 선택하여 로그인 프로세스를 시작합니다. 처음 로그인할 때 다음 스크린샷과 같이 애플리케이션에서 로그인하고 프로필에 액세스할 수 있도록 동의를 제공하라는 메시지가 표시됩니다.
사용자 동의 화면Screenshot displaying user consent screen
Screenshot displaying user consent screen표시하는스크린샷
성공적으로 로그인하면 애플리케이션 홈페이지로 다시 리디렉션됩니다.