在 Node.js Web 应用中编辑个人资料

适用于绿色圆圈,带有白色复选标记符号,指示以下内容适用于外部租户。 外部租户(了解详细信息

本文是一系列的第 2 部分,演示如何在 Node.js Web 应用中添加配置文件编辑逻辑。 在本系列文章的第 1 部分,你设置了应用以进行个人资料编辑。

在本操作指南中,你将了解如何调用 Microsoft Graph API 进行个人资料编辑。

先决条件

完成客户端 Web 应用

在本部分中,将添加客户端 Web 应用的标识相关代码。

更新 authConfig.js 文件

更新客户端 Web 应用的 authConfig.js 文件:

  1. 在代码编辑器中,打开 App/authConfig.js 文件,然后添加三个新变量,GRAPH_API_ENDPOINTGRAPH_ME_ENDPOINT以及editProfileScope。 请确保导出三个变量:

    //...
    const GRAPH_API_ENDPOINT = process.env.GRAPH_API_ENDPOINT || "https://graph.microsoft.com/";
    // https://free.blessedness.top/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 变量表示受 MFA 保护的资源,即中间层应用(EditProfileService 应用)。

    • GRAPH_ME_ENDPOINT 是 Microsoft Graph API 终结点。

  2. 将占位符 {clientId} 替换为前面注册的中间层应用(EditProfileService 应用)的应用程序(客户端)ID。

在客户端 Web 应用中获取访问令牌

在代码编辑器中,打开 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链接时,将触发路由。 应用:

    1. 获取具有 User.Read 权限的访问令牌。
    2. 调用 Microsoft Graph API 以读取已登录用户的个人资料。
    3. gatedUpdateProfile.hbs UI 中显示用户详细信息。
  • 当用户想要更新其显示名称时触发 /updateProfile 路由,即选择 “编辑配置文件 ”按钮。 应用:

    1. 使用 editProfileScope 范围调用中间层应用(EditProfileService 应用)。 通过调用中间层应用(EditProfileService 应用),用户必须完成 MFA 质询(如果尚未这样做)。
    2. updateProfile.hbs UI 中显示用户详细信息。
  • 当用户在 /updateupdateProfile.hbs 中选择“保存”按钮时触发路由。 应用:

    1. 检索应用会话的访问令牌。 你将了解中间层应用(EditProfileService 应用)在下一部分中如何获取访问令牌。
    2. 收集所有用户详细信息。
    3. 调用 Microsoft Graph API 以更新用户的个人资料。

更新 fetch.js 文件

应用使用 App/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 应用)的标识相关代码。

  1. 在代码编辑器中,打开 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
        clientSecret: process.env.CLIENT_SECRET || "Enter_the_Client_Secret_Here ", // Client secret generated from the app registration in Microsoft Entra admin center
      },
      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://free.blessedness.top/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/
  2. 在代码编辑器中,打开 Api/fetch.js 文件,然后粘贴 Api/fetch.js 文件中的代码。 该 fetch 函数使用访问令牌和资源终结点进行实际的 API 调用。

  3. 在代码编辑器中,打开 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图形 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 });
      });

测试你的应用

若要测试应用,请使用以下步骤:

  1. 若要运行客户端应用,请形成终端窗口,导航到 应用 目录,然后运行以下命令:

    npm start
    
  2. 若要运行客户端应用,请形成终端窗口,导航到 API 目录,然后运行以下命令:

    npm start
    
  3. 打开浏览器,然后转到 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'
    
  4. 选择“登录”按钮,然后登录。

  5. 在登录页上,键入你的“电子邮件地址”,选择“下一步”,键入你的“密码”,然后选择“登录”。 如果没有帐户,请选择“无帐户? 创建一个”链接,以启动注册流。

  6. 若要更新个人资料,请选择“个人资料编辑”链接。 此时会看到类似于以下屏幕截图的页面:

    用户更新个人资料的屏幕截图。

  7. 若要编辑个人资料,请选择“个人资料编辑”按钮。 如果尚未执行此操作,应用程序会提示你完成 MFA 质询。

  8. 更改任何个人资料详细信息,然后选择“保存”按钮