Node.js 웹앱에서 프로필 편집
적용 대상: 인력 테넌트 외부 테넌트(자세한 정보)
이 문서는 Node.js 웹앱에서 프로필 편집 논리를 추가하는 방법을 보여 주는 시리즈의 2부입니다. 이 시리즈의 1부에서는 프로필 편집을 위해 앱을 설정합니다.
이 방법 가이드에서는 프로필 편집을 위해 Microsoft Graph API를 호출하는 방법을 알아봅니다.
필수 조건
- 이 가이드 시리즈의 두 번째 부분에서 단계를 완료하고 프로필 편집을 위해 Node.js 웹 애플리케이션을 설정합니다.
클라이언트 웹앱 완료
이 섹션에서는 클라이언트 웹앱에 대한 ID 관련 코드를 추가합니다.
authConfig.js 파일 업데이트
클라이언트 웹앱에 대한 authConfig.js 파일을 업데이트합니다.
코드 편집기에서 App/authConfig.js 파일을 연 다음 세 개의 새 변수를
GRAPH_API_ENDPOINT
GRAPH_ME_ENDPOINT
editProfileScope
추가하고 . 다음 세 가지 변수를 내보내야 합니다.//... const GRAPH_API_ENDPOINT = process.env.GRAPH_API_ENDPOINT || "https://graph.microsoft.com/"; // https://learn.microsoft.com/graph/api/user-update?tabs=http const GRAPH_ME_ENDPOINT = GRAPH_API_ENDPOINT + "v1.0/me"; const editProfileScope = process.env.EDIT_PROFILE_FOR_CLIENT_WEB_APP || 'api://{clientId}/EditProfileService.ReadWrite'; module.exports = { //... editProfileScope, GRAPH_API_ENDPOINT, GRAPH_ME_ENDPOINT, //... };
변수는
editProfileScope
중간 계층 앱(EditProfileService 앱)인 MFA로 보호된 리소스를 나타냅니다.Microsoft
GRAPH_ME_ENDPOINT
Graph API 엔드포인트입니다.
자리 표시자를
{clientId}
이전에 등록한 중간 계층 앱(EditProfileService 앱)의 애플리케이션(클라이언트) ID로 바꿉니다.
클라이언트 웹앱에서 액세스 토큰 획득
코드 편집기에서 App/auth/AuthProvider.js 파일을 연 다음 클래스에서 getToken
메서드를 AuthProvider
업데이트합니다.
class AuthProvider {
//...
getToken(scopes, redirectUri = "http://localhost:3000/") {
return async function (req, res, next) {
const msalInstance = authProvider.getMsalInstance(authProvider.config.msalConfig);
try {
msalInstance.getTokenCache().deserialize(req.session.tokenCache);
const silentRequest = {
account: req.session.account,
scopes: scopes,
};
const tokenResponse = await msalInstance.acquireTokenSilent(silentRequest);
req.session.tokenCache = msalInstance.getTokenCache().serialize();
req.session.accessToken = tokenResponse.accessToken;
next();
} catch (error) {
if (error instanceof msal.InteractionRequiredAuthError) {
req.session.csrfToken = authProvider.cryptoProvider.createNewGuid();
const state = authProvider.cryptoProvider.base64Encode(
JSON.stringify({
redirectTo: redirectUri,
csrfToken: req.session.csrfToken,
})
);
const authCodeUrlRequestParams = {
state: state,
scopes: scopes,
};
const authCodeRequestParams = {
state: state,
scopes: scopes,
};
authProvider.redirectToAuthCodeUrl(
req,
res,
next,
authCodeUrlRequestParams,
authCodeRequestParams,
msalInstance
);
}
next(error);
}
};
}
}
//...
이 메서드는 getToken
지정된 범위를 사용하여 액세스 토큰을 획득합니다. redirectUri
매개 변수는 앱이 액세스 토큰을 획득한 후의 리디렉션 URL입니다.
users.js 파일 업데이트
코드 편집기에서 앱/경로/users.js 파일을 연 다음, 다음 경로를 추가합니다.
//...
var { fetch } = require("../fetch");
const { GRAPH_ME_ENDPOINT, editProfileScope } = require('../authConfig');
//...
router.get(
"/gatedUpdateProfile",
isAuthenticated,
authProvider.getToken(["User.Read"]), // check if user is authenticated
async function (req, res, next) {
const graphResponse = await fetch(
GRAPH_ME_ENDPOINT,
req.session.accessToken,
);
if (!graphResponse.id) {
return res
.status(501)
.send("Failed to fetch profile data");
}
res.render("gatedUpdateProfile", {
profile: graphResponse,
});
},
);
router.get(
"/updateProfile",
isAuthenticated, // check if user is authenticated
authProvider.getToken(
["User.Read", editProfileScope],
"http://localhost:3000/users/updateProfile",
),
async function (req, res, next) {
const graphResponse = await fetch(
GRAPH_ME_ENDPOINT,
req.session.accessToken,
);
if (!graphResponse.id) {
return res
.status(501)
.send("Failed to fetch profile data");
}
res.render("updateProfile", {
profile: graphResponse,
});
},
);
router.post(
"/update",
isAuthenticated,
authProvider.getToken([editProfileScope]),
async function (req, res, next) {
try {
if (!!req.body) {
let body = req.body;
fetch(
"http://localhost:3001/updateUserInfo",
req.session.accessToken,
"POST",
{
displayName: body.displayName,
givenName: body.givenName,
surname: body.surname,
},
)
.then((response) => {
if (response.status === 204) {
return res.redirect("/");
} else {
next("Not updated");
}
})
.catch((error) => {
console.log("error,", error);
});
} else {
throw { error: "empty request" };
}
} catch (error) {
next(error);
}
},
);
//...
고객 사용자가 프로필 편집 링크를 선택하면 경로를 트리거
/gatedUpdateProfile
합니다. 앱은 다음을 수행합니다.- User.Read 권한을 사용하여 액세스 토큰을 획득합니다.
- Microsoft Graph API를 호출하여 로그인한 사용자의 프로필을 읽습니다.
- gatedUpdateProfile.hbs UI에 사용자 세부 정보를 표시합니다.
사용자가 표시 이름을 업데이트하려는 경우 경로를 트리거
/updateProfile
합니다. 즉, 프로필 편집 단추를 선택합니다. 앱은 다음을 수행합니다.- editProfileScope 범위를 사용하여 중간 계층 앱(EditProfileService 앱)을 호출합니다. 중간 계층 앱(EditProfileService 앱)을 호출하여 사용자가 MFA 챌린지를 아직 완료하지 않은 경우 완료해야 합니다.
- updateProfile.hbs UI에 사용자 세부 정보를 표시합니다.
사용자가 gatedUpdateProfile.hbs 또는 updateProfile.hbs에서 저장 단추를 선택하면 경로를 트리거
/update
합니다. 앱은 다음을 수행합니다.- 앱 세션에 대한 액세스 토큰을 검색합니다. 중간 계층 앱(EditProfileService 앱)이 다음 섹션에서 액세스 토큰을 획득하는 방법을 알아봅니다.
- 모든 사용자 세부 정보를 수집합니다.
- Microsoft Graph API를 호출하여 사용자의 프로필을 업데이트합니다.
fetch.js 파일 업데이트
앱은 앱/fetch.js 파일을 사용하여 실제 API를 호출합니다.
코드 편집기에서 App/fetch.js 파일을 연 다음 PATCH 작업 옵션을 추가합니다. 파일을 업데이트한 후 결과 파일은 다음 코드와 유사하게 표시됩니다.
var axios = require('axios');
var authProvider = require("./auth/AuthProvider");
/**
* Makes an Authorization "Bearer" request with the given accessToken to the given endpoint.
* @param endpoint
* @param accessToken
* @param method
*/
const fetch = async (endpoint, accessToken, method = "GET", data = null) => {
const options = {
headers: {
Authorization: `Bearer ${accessToken}`,
},
};
console.log(`request made to ${endpoint} at: ` + new Date().toString());
switch (method) {
case 'GET':
const response = await axios.get(endpoint, options);
return await response.data;
case 'POST':
return await axios.post(endpoint, data, options);
case 'DELETE':
return await axios.delete(endpoint + `/${data}`, options);
case 'PATCH':
return await axios.patch(endpoint, ReqBody = data, options);
default:
return null;
}
};
module.exports = { fetch };
중간 계층 앱 완료
이 섹션에서는 중간 계층 앱(EditProfileService 앱)에 대한 ID 관련 코드를 추가합니다.
코드 편집기에서 Api/authConfig.js 파일을 연 다음, 다음 코드를 추가합니다.
require("dotenv").config({ path: ".env.dev" }); const TENANT_SUBDOMAIN = process.env.TENANT_SUBDOMAIN || "Enter_the_Tenant_Subdomain_Here"; const TENANT_ID = process.env.TENANT_ID || "Enter_the_Tenant_ID_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"; /** * 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_Edit_Profile_Service_Application_Id_Here", // 'Application (client) ID' of the Edit_Profile Service App registration in Microsoft Entra admin center - this value is a GUID authority: process.env.AUTHORITY || `https://${TENANT_SUBDOMAIN}.ciamlogin.com/`, // Replace the placeholder with your external tenant name }, system: { loggerOptions: { loggerCallback(loglevel, message, containsPii) { console.log(message); }, piiLoggingEnabled: false, logLevel: "Info", }, }, }; const GRAPH_API_ENDPOINT = process.env.GRAPH_API_ENDPOINT || "graph_end_point"; // Refers to the user that is single user singed in. // https://learn.microsoft.com/en-us/graph/api/user-update?tabs=http const GRAPH_ME_ENDPOINT = GRAPH_API_ENDPOINT + "v1.0/me"; module.exports = { msalConfig, REDIRECT_URI, POST_LOGOUT_REDIRECT_URI, TENANT_SUBDOMAIN, GRAPH_API_ENDPOINT, GRAPH_ME_ENDPOINT, TENANT_ID, };
자리 표시자 찾기:
Enter_the_Tenant_Subdomain_Here
디렉터리(테넌트) 하위 도메인으로 대체합니다. 예를 들어, 테넌트 기본 도메인이contoso.onmicrosoft.com
인 경우contoso
를 사용합니다. 테넌트 이름이 없는 경우 테넌트 세부 정보를 읽는 방법을 알아봅니다.Enter_the_Tenant_ID_Here
테넌트 ID로 대체합니다. 테넌트 ID가 없는 경우 테넌트 세부 정보를 읽는 방법을 알아봅니다.Enter_the_Edit_Profile_Service_Application_Id_Here
앞에서 등록한 EditProfileService의 애플리케이션(클라이언트) ID 값으로 바꿉니다.Enter_the_Client_Secret_Here
이전에 복사한 EditProfileService 앱 비밀 값으로 바꿉니다.graph_end_point
를 Microsoft Graph API 엔드포인트인https://graph.microsoft.com/
으로 바꿉니다.
코드 편집기에서 Api/fetch.js 파일을 연 다음 Api/fetch.js 파일의 코드를 붙여넣습니다. 이 함수는
fetch
액세스 토큰 및 리소스 엔드포인트를 사용하여 실제 API 호출을 만듭니다.코드 편집기에서 Api/index.js 파일을 연 다음 Api/index.js 파일의 코드를 붙여넣습니다.
acquireTokenOnBehalfOf를 사용하여 액세스 토큰 획득
Api/index.js 파일에서 중간 계층 앱(EditProfileService 앱)은 acquireTokenOnBehalfOf 함수를 사용하여 액세스 토큰을 획득합니다. 이 함수는 해당 사용자를 대신하여 프로필을 업데이트하는 데 사용합니다.
async function getAccessToken(tokenRequest) {
try {
const response = await cca.acquireTokenOnBehalfOf(tokenRequest);
return response.accessToken;
} catch (error) {
console.error("Error acquiring token:", error);
throw error;
}
}
매개 tokenRequest
변수는 다음 코드와 같이 정의됩니다.
const tokenRequest = {
oboAssertion: req.headers.authorization.replace("Bearer ", ""),
authority: `https://${TENANT_SUBDOMAIN}.ciamlogin.com/${TENANT_ID}`,
scopes: ["User.ReadWrite"],
correlationId: `${uuidv4()}`,
};
동일한 파일인 API/index.js 중간 계층 앱(EditProfileService 앱)에서 Microsoft Graph API를 호출하여 사용자의 프로필을 업데이트합니다.
let accessToken = await getAccessToken(tokenRequest);
fetch(GRAPH_ME_ENDPOINT, accessToken, "PATCH", req.body)
.then((response) => {
if (response.status === 204) {
res.status(response.status);
res.json({ message: "Success" });
} else {
res.status(502);
res.json({ message: "Failed, " + response.body });
}
})
.catch((error) => {
res.status(502);
res.json({ message: "Failed, " + error });
});
앱 테스트
앱을 테스트하려면 다음 단계를 사용합니다.
클라이언트 앱을 실행하려면 터미널 창을 형성하고 앱 디렉터리로 이동한 다음 다음 명령을 실행합니다.
npm start
클라이언트 앱을 실행하려면 터미널 창을 형성하고 API 디렉터리로 이동한 다음 다음 명령을 실행합니다.
npm start
브라우저를 연 다음 http://localhost:3000.으로 이동합니다. SSL 인증서 오류가 발생하는 경우 파일을 만든
.env
다음, 다음 구성을 추가합니다.# Use this variable only in the development environment. # Remove the variable when you move the app to the production environment. NODE_TLS_REJECT_UNAUTHORIZED='0'
로그인 단추를 선택한 다음 로그인합니다.
로그인 페이지에서 이메일 주소를 입력하고, 다음을 선택하고, 암호를 입력한 다음, 로그인을 선택합니다. 계정이 없는 경우 등록 흐름을 시작하는 계정이 없으신가요? 만들기 링크를 선택합니다.
프로필을 업데이트하려면 프로필 편집 링크를 선택합니다. 다음 스크린샷과 비슷한 페이지가 표시됩니다.
프로필을 편집하려면 프로필 편집 단추를 선택합니다. 아직 수행하지 않은 경우 앱에서 MFA 챌린지를 완료하라는 메시지를 표시합니다.
프로필 세부 정보를 변경한 다음 저장 단추를 선택합니다.
관련 콘텐츠
- Microsoft ID 플랫폼 및 OAuth 2.0 On-Behalf-Of 흐름에 대해 자세히 알아봅니다.