在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缺失,导致拦截器无法获取所需数据。
核心问题
- 如何获取Id Token和Refresh Token?
microsoftTeams.authentication.getAuthToken是否仅返回Access Token?- 是否需要将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缓存:
- 调用
microsoftTeams.authentication.authenticate打开你的Angular登录页。 - 在登录页使用MSAL标准流程完成认证,获取Id Token、Access Token和Refresh Token。
- 通过
microsoftTeams.authentication.notifySuccess将令牌传递回标签页应用。 - 将令牌存入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).

