You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

配置OneDrive FilePicker v8 for Business/SharePoint遇受众URI验证失败异常

现有配置信息

AAD应用配置

{
    "id": "***",
    "acceptMappedClaims": null,
    "accessTokenAcceptedVersion": null,
    "addIns": [],
    "allowPublicClient": null,
    "appId": "***",
    "appRoles": [],
    "oauth2AllowUrlPathMatching": false,
    "createdDateTime": "2023-01-17T13:20:32Z",
    "description": null,
    "certification": null,
    "disabledByMicrosoftStatus": null,
    "groupMembershipClaims": null,
    "identifierUris": [],
    "informationalUrls": {
        "termsOfService": "https://***.com/app/tos",
        "support": null,
        "privacy": "https://***.com/app/privacy",
        "marketing": null
    },
    "keyCredentials": [],
    "knownClientApplications": [],
    "logoUrl": "https://aadcdn.msftauthimages.net/c1c6b6c8-v-6zmda-jxeuzcj5pwn1sghjyu3gaqcdbwxfx543nak/appbranding/caofuabptruma-iurozspazfz7p4ilc95fi3qyhjlu0/1033/bannerlogo?ts=638095585782399340",
    "logoutUrl": null,
    "name": "File Picker (SharePoint)",
    "notes": null,
    "oauth2AllowIdTokenImplicitFlow": false,
    "oauth2AllowImplicitFlow": false,
    "oauth2Permissions": [],
    "oauth2RequirePostResponse": false,
    "optionalClaims": null,
    "orgRestrictions": [],
    "parentalControlSettings": {
        "countriesBlockedForMinors": [],
        "legalAgeGroupRule": "Allow"
    },
    "passwordCredentials": [],
    "preAuthorizedApplications": [],
    "publisherDomain": "***.com",
    "replyUrlsWithType": [
        {
            "url": "https://localhost",
            "type": "Spa"
        },
        {
            "url": "http://localhost",
            "type": "Spa"
        },
        {
            "url": "https://***.com/onedrive/search",
            "type": "Spa"
        }
    ],
    "requiredResourceAccess": [<see next image>],
    "samlMetadataUrl": null,
    "signInUrl": "https://***.com",
    "signInAudience": "AzureADMultipleOrgs",
    "tags": [],
    "tokenEncryptionKeyId": null
}

AAD权限范围

AAD权限范围截图

浏览器端实现代码

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="/public/css/style.css">
  <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css">
  <title>File Picker</title>
</head>
<body>
  <div class="container-lg pt-3">
    <h3 class="d-flex align-items-center"><span>OneDrive File Picker</span></h3>
    <div><button class="btn btn-primary" id="launchPicker">Pick from OneDrive</button></div>
    <iframe class="w-100 mt-3" id="iframe" frameborder="0" style="min-height:600px;resize:vertical;"></iframe>
  </div>
  <script type="text/javascript" src="https://alcdn.msauth.net/browser/2.19.0/js/msal-browser.min.js"
    nonce="392b4bdf6481a24edf09d81187872ce7"></script>
  <script type="text/javascript" nonce="392b4bdf6481a24edf09d81187872ce7">
    const SUB = "***"; // Test user
    const REDIRECT_URI = `https://***.com/onedrive/search` // Test redirect url
    const BASE_URL = `https://***-my.sharepoint.com/` // Test url
    const msalParams =
    {
      auth:
      {
        authority: "https://login.microsoftonline.com/organizations", // Full directory URL, in the form of https://login.microsoftonline.com/<tenant>
        clientId: "***", // 'Application (client) ID' of app registration in Azure portal - this value is a GUID
        redirectUri: REDIRECT_URI
      },
    }
    const app = new msal.PublicClientApplication(msalParams);
    const redirectResponse = app.handleRedirectPromise();
    async function getToken() {
      let accessToken = "";
      const account = await app.getAccountByLocalId(SUB);
      if (await redirectResponse !== null) {
        // Acquire token silent success
        accessToken = redirectResponse.accessToken;
      }
      else {
        authParams =
        {
          scopes: [`${BASE_URL}.default`],
          account: account
        };
        try {
          // see if we have already the idtoken saved
          const resp = await app.acquireTokenSilent(authParams);
          accessToken = resp.accessToken;
        }
        catch (e) {
          // per examples we fall back to popup
          return app.acquireTokenRedirect(authParams);
        }
      }
      return accessToken;
    }
    // the options we pass to the picker page through the querystring
    const params =
    {
      sdk: "8.0",
      entry:
      {
        oneDrive:
        {
          files: {},
        }
      },
      authentication: {},
      messaging:
      {
        origin: window.location.href,
        channelId: "27"
      },
      typesAndSources:
      {
        mode: "files",
        pivots:
        {
          oneDrive: true,
          recent: true,
          sharedLibraries: true,
        },
        //- filters: ".csv,.xlsx"
      },
      selection:
      {
        mode: "pick"
      },
      commands:
      {
        pick:
        {
          label: "Import"
        },
        close:
        {
          label: "Close"
        }
      }
    };
    async function launchPicker(e) {
      e.preventDefault();
      //- win = window.open("", "Picker", "width=800,height=600")
      const frame = document.getElementById("iframe");
      const win = frame.contentWindow;
      const authToken = await getToken();
      const queryString = new URLSearchParams(
        {
          filePicker: JSON.stringify(params),
        });
      const url = `${BASE_URL}_layouts/15/FilePicker.aspx?${queryString}`;
      const form = win.document.createElement("form");
      form.setAttribute("action", url);
      form.setAttribute("method", "POST");
      win.document.body.append(form);
      const input = win.document.createElement("input");
      input.setAttribute("type", "hidden")
      input.setAttribute("name", "access_token");
      input.setAttribute("value", authToken);
      form.appendChild(input);
      form.submit();
      window.addEventListener("message", (event) => {
        if (event.source && event.source === win) {
          const message = event.data;
          if (message.type === "initialize" && message.channelId === params.messaging.channelId) {
            port = event.ports[0];
            port.addEventListener("message", messageListener);
            port.start();
            port.postMessage(
              {
                type: "activate",
              });
          }
        }
      });
    }
    async function messageListener(message) {
      switch (message.data.type) {
        case "notification":
          console.log(`notification: ${message.data}`);
          break;
        case "command":
          port.postMessage(
            {
              type: "acknowledge",
              id: message.data.id,
            });
          const command = message.data.data;
          switch (command.command) {
            case "authenticate":
              // getToken is from scripts/auth.js
              const token = await getToken();
              if (typeof token !== "undefined" && token !== null) {
                port.postMessage(
                  {
                    type: "result",
                    id: message.data.id,
                    data:
                    {
                      result: "token",
                      token,
                    }
                  });
              }
              else {
                console.error(`Could not get auth token for command: ${JSON.stringify(command)}`);
              }
              break;
            case "close":
              history.back();
              break;
            case "pick":
              console.log(`Picked: ${JSON.stringify(command)}`);
              port.postMessage(
                {
                  type: "result",
                  id: message.data.id,
                  data: {
                    result: "success",
                  },
                });
              console.info(command)
              break;
            default:
              console.warn(`Unsupported command: ${JSON.stringify(command)}`, 2);
              port.postMessage(
                {
                  result: "error",
                  error:
                  {
                    code: "unsupportedCommand",
                    message: command.command
                  },
                  isExpected: true,
                });
              break;
          }
          break;
      }
    }
    document.getElementById("launchPicker").onclick = launchPicker;
    // Check the token directly and reload when no access token acquirable
    getToken().then((accessToken) => {
      if (!accessToken) {
        console.info("Something went wrong acquiring a token, reloading the page and try again!")
        window.location.reload();
      }
    })
  </script>
  <footer></footer>
</body>
</html>

解决方案

核心问题定位

Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException本质是令牌的受众(Audience)与SharePoint验证时期望的受众不匹配,结合你的配置,主要问题集中在AAD应用配置和令牌请求逻辑。

1. AAD应用配置修正

  • 添加标识符URI:当前配置中identifierUris为空,企业环境下SharePoint需要应用有明确的标识符URI才能验证令牌。操作步骤:
    1. 进入Azure AD应用注册→概述→点击「添加标识符URI」
    2. 输入格式如api://<你的客户端ID>或符合企业域名的URI,保存后identifierUris会生成有效值
  • 补全SharePoint权限:从权限截图确认已添加SharePoint Online的Delegated权限(如Sites.Read.All、Files.Read.All),并完成管理员同意(租户级应用需此步骤)

2. JavaScript代码调整

  • 修改令牌请求作用域:将原${BASE_URL}.default替换为Microsoft Graph统一作用域,确保覆盖SharePoint站点访问权限:
    // 替换原authParams中的scopes
    scopes: ["https://graph.microsoft.com/Sites.Read.All", "https://graph.microsoft.com/Files.Read.All"]
    
  • 验证令牌受众:获取令牌后,用本地JWT解码工具查看aud字段,确认其为https://graph.microsoft.com(使用Graph作用域时)或正确的SharePoint租户受众
  • FilePicker参数优化:确保typesAndSources.pivots.sharedLibraries为true的同时,可尝试将FilePicker切换为Microsoft Graph驱动模式(更适配企业多站点场景)

3. SharePoint权限验证

  • 确认测试用户拥有目标SharePoint站点/文件夹的至少读取权限
  • 检查站点是否设置了严格的访问控制(如仅限特定安全组),确保测试用户在允许范围内

验证步骤

  1. 完成AAD应用标识符URI配置并保存
  2. 更新代码中的令牌请求作用域
  3. 重新获取令牌并验证受众正确性
  4. 重启应用测试SharePoint文件夹访问

内容的提问来源于stack exchange,提问作者Miaucl

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 12:15:26