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

在Microsoft Teams中运行的Angular应用如何通过MSAL获取Id Token与Refresh Token

问题背景

我正在使用teams-angular项目开发作为Microsoft Teams标签页运行的Angular应用。在浏览器中单独测试该Angular Web应用时,应用运行正常:登录成功后,MSAL拦截器可从localStorage获取所有必要数据(Access Token、Id Token和Refresh Token均已填充),并在请求头中添加正确的令牌以调用Microsoft Graph API。但在Teams中运行时,仅Access Token已填充,Id Token和Refresh Token缺失,导致拦截器无法获取所需数据。

核心问题

  1. 如何获取Id Token和Refresh Token?
  2. microsoftTeams.authentication.getAuthToken是否仅返回Access Token?
  3. 是否需要将Id Token和Refresh Token存入localStorage才能成功调用Microsoft Graph API?

相关代码

teams.guard.ts

if (inTeams) {
  return new Promise<boolean | UrlTree>((resolve) => {
    microsoftTeams.authentication.getAuthToken({
      successCallback: (token: string) => {
        const decodedToken: { [key: string]: any; } = jwtDecode(token) as { [key: string]: any; };
        this.registerTeamsTokenWithMsal(decodedToken, token);
        microsoftTeams.appInitialization.notifySuccess();
        resolve(true);
      },
      failureCallback: (message: string) => {
        microsoftTeams.appInitialization.notifyFailure({
          reason: microsoftTeams.appInitialization.FailedReason.AuthFailed,
          message
        });
        console.log("notifyFailure: " + microsoftTeams.appInitialization.FailedReason.AuthFailed +
                    ", message: " + message);
        this.authService.redirectUrl = state.url;
        resolve(this.router.parseUrl('/login'));
      },
      resources: [
        'https://xxx.ngrok-free.app',
        'https://graph.microsoft.com/v1.0/me'
      ]
    });
  });
}

private registerTeamsTokenWithMsal(accessToken: { [key: string]: any; }, accessTokenString: string): void {
  const accountEntity = this.getAccountEntity(accessToken);
  const accessTokenEntity = this.getAccessTokenEntity(accessToken, accessTokenString);
  
  const browserStorage = (this.msalService.instance as any).browserStorage;
  browserStorage.setAccount(accountEntity);
  browserStorage.setAccessTokenCredential(accessTokenEntity);
  this.msalService.instance.setActiveAccount(this.msalService.instance.getAllAccounts()[0]);
}


private getAccountEntity(accessToken: { [key: string]: any; }): AccountEntity {
  const account = new AccountEntity();
  Object.assign(account, {
    authorityType: 'MSSTS',
    environment: 'login.windows.net',
    homeAccountId: `${accessToken['oid']}.${accessToken['tid']}`,
    localAccountId: accessToken['oid'],
    idTokenClaims: accessToken,
    realm: accessToken['tid'],
    username: accessToken['upn']
  });

  return account;
}

private getAccessTokenEntity(accessToken: { [key: string]: any; }, accessTokenString: string): AccessTokenEntity {
  const accessTokenEntity = new AccessTokenEntity();
  Object.assign(accessTokenEntity, {
    cachedAt: accessToken['iat'],
    clientId: (accessToken['aud'] as string).substring((accessToken['aud'] as string).lastIndexOf('/') + 1),
    credentialType: 'AccessToken',
    environment: 'login.windows.net',
    expiresOn: accessToken['exp'],
    extendedExpiresOn: accessToken['exp'],
    homeAccountId: `${accessToken['oid']}.${accessToken['tid']}`,
    realm: accessToken['tid'],
    secret: accessTokenString,
    target: accessToken['scp'],
    tokenType: 'Bearer'
  });

  return accessTokenEntity;
}

home.component.ts

callProfile () {
  this.http.get("https://graph.microsoft.com/v1.0/me").subscribe(
    resp  => {
      this.apiResponse = resp;
    },
    error => {
      console.error('error caught in component')
      console.log(error?.message)
      throw error;
    }
  )
}

app.module.ts

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    HomeComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    AppRoutingModule,
    MsalModule.forRoot(new PublicClientApplication({
      auth: {
        clientId: environment.aadAppId,
        authority: `https://login.microsoftonline.com/${environment.tenantId}`,
        redirectUri: 'https://xxx.ngrok-free.app',
        navigateToLoginRequestUrl: true
      },
      cache: {
        cacheLocation: BrowserCacheLocation.LocalStorage,
        storeAuthStateInCookie: isIE,
      },
      system: {
        loggerOptions: {
           loggerCallback: (logLevel, message, containsPii) => {
               console.log(message);
            },
            logLevel: LogLevel.Verbose,
            piiLoggingEnabled: false
        }
      },
    }),
    {
      interactionType: InteractionType.Redirect,
      authRequest: {
        scopes: ['user.read','group.Read.All','access_as_user','profile','offline_access','openid'],
      },
    },
    {
      interactionType: InteractionType.Redirect,
      protectedResourceMap: new Map([
        ["https://xxx.ngrok-free.app",["access_as_user"]],
        ["https://graph.microsoft.com/v1.0/me", ["user.read"]],
        ["https://graph.microsoft.com/v1.0/groups", ["group.Read.All"]]
      ])
    }
    )
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: MsalInterceptor,
      multi: true
    }
  ],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  bootstrap: [AppComponent]
})
export class AppModule { }

控制台输出

@azure/msal-angular@2.5.5 : Verbose - MSAL Interceptor activated main.js:169:37
@azure/msal-angular@2.5.5 : Verbose - Interceptor - getting scopes for endpoint main.js:169:37
@azure/msal-common@12.0.0 : Info - CacheManager:getIdToken - No token found main.js:169:37
@azure/msal-angular@2.5.5 : Verbose - Interceptor - active account selected main.js:169:37
@azure/msal-common@12.0.0 : Info - CacheManager:getIdToken - No token found main.js:169:37
@azure/msal-angular@2.5.5 : Info - Interceptor - 1 scopes found for endpoint main.js:169:37
@azure/msal-browser@2.35.0 : Verbose - preflightBrowserEnvironmentCheck started main.js:169:37
[some-id] : @azure/msal-browser@2.35.0 : Verbose - acquireTokenSilent called main.js:169:37
[some-id] : @azure/msal-browser@2.35.0 : Verbose - acquireTokenSilent called for the first time, storing active request main.js:169:37
@azure/msal-browser@2.35.0 : Info - Emitting event: msal:acquireTokenStart main.js:169:37
@azure/msal-browser@2.35.0 : Verbose - Emitting event to callback 2fcdf349-4ebc-4bad-a8a2-1b6b1a3fc81f: msal:acquireTokenStart main.js:169:37
@azure/msal-browser@2.35.0 : Verbose - acquireTokenSilent - attempting to acquire token from web flow main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Initializing BaseAuthRequest main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Authentication Scheme wasn't explicitly set in request, defaulting to "Bearer" request main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - initializeServerTelemetryManager called main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - getClientConfiguration called main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - getDiscoveredAuthority called main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Creating discovered authority with configured authority main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Attempting to get cloud discovery metadata in the config main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Did not find cloud discovery metadata in the config... Attempting to get cloud discovery metadata from the cache. main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Did not find cloud discovery metadata in the cache... Attempting to get cloud discovery metadata from the network. main.js:169:37
@azure/msal-common@12.0.0 : Info - CacheManager:getIdToken - No token found 8 main.js:169:37
@azure/msal-common@12.0.0 : Info - CacheManager:getIdToken - No token found 8 main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Attempting to find a match between the developer's authority and the CloudInstanceDiscoveryMetadata returned from the network request. main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - cloud discovery metadata was successfully returned from getCloudDiscoveryMetadataFromNetwork() main.js:169:37
@azure/msal-common@12.0.0 : Info - CacheManager:getIdToken - No token found 8 main.js:169:37
@azure/msal-common@12.0.0 : Info - CacheManager:getIdToken - No token found 8 main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Silent auth client created main.js:169:37
@azure/msal-common@12.0.0 : Info - CacheManager:getIdToken - No token found main.js:169:37
@azure/msal-common@12.0.0 : Info - CacheManager:getAccessToken - No token found main.js:169:37
@azure/msal-common@12.0.0 : Info - CacheManager:getRefreshToken - No refresh token found. main.js:169:37
[some-id] : @azure/msal-common@12.0.0 : Info - SilentFlowClient:acquireCachedToken - No access token found in cache for the given properties. main.js:169:37
@azure/msal-browser@2.35.0 : Info - Emitting event: msal:acquireTokenFromNetworkStart main.js:169:37
@azure/msal-browser@2.35.0 : Verbose - Emitting event to callback 2fcdf349-4ebc-4bad-a8a2-1b6b1a3fc81f: msal:acquireTokenFromNetworkStart main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Initializing BaseAuthRequest main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Authentication Scheme set to "Bearer" as configured in Auth request main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - initializeServerTelemetryManager called main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - getClientConfiguration called main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - getDiscoveredAuthority called main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Creating discovered authority with configured authority main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Attempting to get cloud discovery metadata in the config main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Did not find cloud discovery metadata in the config... Attempting to get cloud discovery metadata from the cache. main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Found metadata in the cache. main.js:169:37
[some-id] : msal.js.browser@2.35.0 : Verbose - Refresh token client created main.js:169:37
[some-id] : @azure/msal-common@12.0.0 : Verbose - RefreshTokenClientAcquireTokenWithCachedRefreshToken called main.js:169:37
@azure/msal-common@12.0.0 : Info - CacheManager:getRefreshToken - No refresh token found. main.js:169:37
@azure/msal-browser@2.35.0 : Info - Emitting event: msal:acquireTokenFailure main.js:169:37
@azure/msal-browser@2.35.0 : Verbose - Emitting event to callback 2fcdf349-4ebc-4bad-a8a2-1b6b1a3fc81f: msal:acquireTokenFailure main.js:169:37
@azure/msal-angular@2.5.5 : Error - Interceptor - acquireTokenSilent rejected with error. Invoking interaction to resolve. main.js:169:37
@azure/msal-common@12.0.0 : Info - CacheManager:getIdToken - No token found

解答

问题2:microsoftTeams.authentication.getAuthToken是否仅返回Access Token?

是,这个API的设计就是只返回针对指定资源的Access Token,不会返回Id Token或Refresh Token。它是Teams提供的简化认证方式,由Teams身份代理处理令牌获取,不会将Id/Refresh Token暴露给标签页应用。

问题3:是否需要将Id Token和Refresh Token存入localStorage才能成功调用Microsoft Graph API?

不一定,但你的当前实现依赖MSAL的缓存机制,所以需要让MSAL认为令牌缓存是完整的。MSAL拦截器调用acquireTokenSilent时,会检查Id Token(验证账户有效性)和Refresh Token(静默刷新Access Token),缺失则触发失败。

你可以选择绕过MSAL的拦截逻辑,直接使用Teams获取的Access Token调用API,也可以修改MSAL缓存填充方式适配当前场景。

问题1:如何获取Id Token和Refresh Token?

由于getAuthToken不提供这两个令牌,有两种可行方案:

方案1:自定义Teams认证流程,从AAD获取完整令牌

放弃getAuthToken,改用Teams弹出式认证窗口直接对接AAD,获取所有令牌后存入MSAL缓存:

  1. 调用microsoftTeams.authentication.authenticate打开你的Angular登录页。
  2. 在登录页使用MSAL标准流程完成认证,获取Id Token、Access Token和Refresh Token。
  3. 通过microsoftTeams.authentication.notifySuccess将令牌传递回标签页应用。
  4. 将令牌存入MSAL缓存,保持和浏览器环境一致的状态。

方案2:修复MSAL缓存填充逻辑,绕过Id/Refresh Token依赖

手动构造Id Token实体存入MSAL缓存,同时禁用自动刷新逻辑(无Refresh Token无法自动刷新):

private registerTeamsTokenWithMsal(accessToken: { [key: string]: any; }, accessTokenString: string): void {
  const accountEntity = this.getAccountEntity(accessToken);
  const accessTokenEntity = this.getAccessTokenEntity(accessToken, accessTokenString);
  // 手动构造Id Token实体(用Access Token的Claims填充)
  const idTokenEntity = new IdTokenEntity();
  Object.assign(idTokenEntity, {
    authorityType: 'MSSTS',
    environment: 'login.windows.net',
    homeAccountId: `${accessToken['oid']}.${accessToken['tid']}`,
    localAccountId: accessToken['oid'],
    realm: accessToken['tid'],
    secret: accessTokenString,
    idTokenClaims: accessToken,
    clientId: (accessToken['aud'] as string).substring((accessToken['aud'] as string).
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 23:37:29