diff --git a/change/msal-12d2764f-51e6-49b0-a48b-39ded19ba8a7.json b/change/msal-12d2764f-51e6-49b0-a48b-39ded19ba8a7.json new file mode 100644 index 0000000000..0f903f19c6 --- /dev/null +++ b/change/msal-12d2764f-51e6-49b0-a48b-39ded19ba8a7.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Fix token cache for /consumers authority #3327", + "packageName": "msal", + "email": "thomas.norling@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/lib/msal-core/src/UserAgentApplication.ts b/lib/msal-core/src/UserAgentApplication.ts index a3aaa6076a..2afca4b4e3 100644 --- a/lib/msal-core/src/UserAgentApplication.ts +++ b/lib/msal-core/src/UserAgentApplication.ts @@ -1381,7 +1381,7 @@ export class UserAgentApplication { private getTokenCacheItemByAuthority(authority: string, tokenCacheItems: Array, requestScopes: Array, tokenType: string): AccessTokenCacheItem { let filteredAuthorityItems: Array; - if (UrlUtils.isCommonAuthority(authority) || UrlUtils.isOrganizationsAuthority(authority)) { + if (UrlUtils.isCommonAuthority(authority) || UrlUtils.isOrganizationsAuthority(authority) || UrlUtils.isConsumersAuthority(authority)) { filteredAuthorityItems = AuthCacheUtils.filterTokenCacheItemsByDomain(tokenCacheItems, UrlUtils.GetUrlComponents(authority).HostNameAndPort); } else { filteredAuthorityItems = AuthCacheUtils.filterTokenCacheItemsByAuthority(tokenCacheItems, authority); @@ -1463,13 +1463,6 @@ export class UserAgentApplication { if (!accessTokenCacheItem) { this.logger.verbose("No matching token found when filtering by scope and authority"); - const authorityList = this.getUniqueAuthority(tokenCacheItems, "authority"); - if (authorityList.length > 1) { - throw ClientAuthError.createMultipleAuthoritiesInCacheError(scopes.toString()); - } - - this.logger.verbose("Single authority used, setting authorityInstance"); - serverAuthenticationRequest.authorityInstance = AuthorityFactory.CreateInstance(authorityList[0], this.config.auth.validateAuthority); return null; } else { serverAuthenticationRequest.authorityInstance = AuthorityFactory.CreateInstance(accessTokenCacheItem.key.authority, this.config.auth.validateAuthority); @@ -1519,25 +1512,6 @@ export class UserAgentApplication { return TokenUtils.validateExpirationIsWithinOffset(expiration, this.config.system.tokenRenewalOffsetSeconds); } - /** - * @hidden - * Used to get a unique list of authorities from the cache - * @param {Array} accessTokenCacheItems - accessTokenCacheItems saved in the cache - * @ignore - */ - private getUniqueAuthority(accessTokenCacheItems: Array, property: string): Array { - this.logger.verbose("GetUniqueAuthority has been called"); - const authorityList: Array = []; - const flags: Array = []; - accessTokenCacheItems.forEach(element => { - if (element.key.hasOwnProperty(property) && (flags.indexOf(element.key[property]) === -1)) { - flags.push(element.key[property]); - authorityList.push(element.key[property]); - } - }); - return authorityList; - } - /** * @hidden * Check if ADAL id_token exists and return if exists. diff --git a/lib/msal-core/src/error/ClientAuthError.ts b/lib/msal-core/src/error/ClientAuthError.ts index e02bb8aaa2..8e585d93e4 100644 --- a/lib/msal-core/src/error/ClientAuthError.ts +++ b/lib/msal-core/src/error/ClientAuthError.ts @@ -8,10 +8,6 @@ import { IdToken } from "../IdToken"; import { StringUtils } from "../utils/StringUtils"; export const ClientAuthErrorMessage = { - multipleCacheAuthorities: { - code: "multiple_authorities", - desc: "Multiple authorities found in the cache. Pass authority in the API overload." - }, endpointResolutionError: { code: "endpoints_resolution_error", desc: "Error: could not resolve endpoints. Please check network and try again." @@ -114,11 +110,6 @@ export class ClientAuthError extends AuthError { return new ClientAuthError(ClientAuthErrorMessage.endpointResolutionError.code, errorMessage); } - static createMultipleAuthoritiesInCacheError(scope: string): ClientAuthError { - return new ClientAuthError(ClientAuthErrorMessage.multipleCacheAuthorities.code, - `Cache error for scope ${scope}: ${ClientAuthErrorMessage.multipleCacheAuthorities.desc}.`); - } - static createPopupWindowError(errDetail?: string): ClientAuthError { let errorMessage = ClientAuthErrorMessage.popUpWindowError.desc; if (errDetail && !StringUtils.isEmpty(errDetail)) { diff --git a/lib/msal-core/src/utils/Constants.ts b/lib/msal-core/src/utils/Constants.ts index 442fc28379..70951d84a1 100644 --- a/lib/msal-core/src/utils/Constants.ts +++ b/lib/msal-core/src/utils/Constants.ts @@ -129,6 +129,7 @@ export enum SSOTypes { SID = "sid", LOGIN_HINT = "login_hint", ORGANIZATIONS = "organizations", + CONSUMERS = "consumers", ID_TOKEN ="id_token", ACCOUNT_ID = "accountIdentifier", HOMEACCOUNT_ID = "homeAccountIdentifier" diff --git a/lib/msal-core/src/utils/UrlUtils.ts b/lib/msal-core/src/utils/UrlUtils.ts index 84d4abda63..7c0462bbb5 100644 --- a/lib/msal-core/src/utils/UrlUtils.ts +++ b/lib/msal-core/src/utils/UrlUtils.ts @@ -95,7 +95,7 @@ export class UrlUtils { const lowerCaseUrl = url.toLowerCase(); const urlObject = this.GetUrlComponents(lowerCaseUrl); const pathArray = urlObject.PathSegments; - if (tenantId && (pathArray.length !== 0 && (pathArray[0] === Constants.common || pathArray[0] === SSOTypes.ORGANIZATIONS))) { + if (tenantId && (pathArray.length !== 0 && (pathArray[0] === Constants.common || pathArray[0] === SSOTypes.ORGANIZATIONS || pathArray[0] === SSOTypes.CONSUMERS))) { pathArray[0] = tenantId; } return this.constructAuthorityUriFromObject(urlObject, pathArray); @@ -127,6 +127,17 @@ export class UrlUtils { return (pathArray.length !== 0 && pathArray[0] === SSOTypes.ORGANIZATIONS); } + /** + * Checks if an authority is for consumers (ex. https://a:b/consumers/) + * @param url The url + * @returns true if authority is for and false otherwise + */ + static isConsumersAuthority(url: string): boolean { + const authority = this.CanonicalizeUri(url); + const pathArray = this.GetUrlComponents(authority).PathSegments; + return (pathArray.length !== 0 && pathArray[0] === SSOTypes.CONSUMERS); + } + /** * Parses out the components from a url string. * @returns An object with the various components. Please cache this value insted of calling this multiple times on the same url. diff --git a/lib/msal-core/test/UserAgentApplication.spec.ts b/lib/msal-core/test/UserAgentApplication.spec.ts index 088562610f..7d9dc930a9 100644 --- a/lib/msal-core/test/UserAgentApplication.spec.ts +++ b/lib/msal-core/test/UserAgentApplication.spec.ts @@ -1140,34 +1140,6 @@ describe("UserAgentApplication.ts Class", function () { }); }); - it("tests getCachedToken without sending authority when no matching accesstoken is found and multiple authorities exist", function (done) { - const tokenRequest : AuthenticationParameters = { - scopes: ["S3"], - account: account - }; - const params: kv = { }; - params[SSOTypes.SID] = account.sid; - setUtilUnifiedCacheQPStubs(params); - - cacheStorage.setItem(JSON.stringify(accessTokenKey), JSON.stringify(accessTokenValue)); - accessTokenKey.scopes = "S2"; - accessTokenKey.authority = TEST_CONFIG.alternateValidAuthority; - cacheStorage.setItem(JSON.stringify(accessTokenKey), JSON.stringify(accessTokenValue)); - cacheStorage.setItem(JSON.stringify(idTokenKey), JSON.stringify(idToken)); - - msal.acquireTokenSilent(tokenRequest).then(function(response) { - // Won't happen - console.error("Shouldn't have response here. Data: " + JSON.stringify(response)); - }).catch(function(err: AuthError) { - expect(err.errorCode).to.include(ClientAuthErrorMessage.multipleCacheAuthorities.code); - expect(err.errorMessage).to.include(ClientAuthErrorMessage.multipleCacheAuthorities.desc); - expect(err.message).to.contain(ClientAuthErrorMessage.multipleCacheAuthorities.desc); - expect(err.name).to.equal("ClientAuthError"); - expect(err.stack).to.include("UserAgentApplication.spec.ts"); - done(); - }); - }); - it("tests getCachedToken when common authority is passed and single matching accessToken is found", function (done) { const tokenRequest : AuthenticationParameters = { authority: TEST_CONFIG.validAuthority, @@ -1273,6 +1245,59 @@ describe("UserAgentApplication.ts Class", function () { }); }); + it("tests getCachedToken when consumers authority is passed and single matching accessToken is found", function (done) { + + const tokenRequest : AuthenticationParameters = { + authority: TEST_URIS.DEFAULT_INSTANCE + "consumers/", + scopes: ["S1"], + account: account + }; + const tokenRequestAlternate : AuthenticationParameters = { + authority: TEST_URIS.ALTERNATE_INSTANCE + "consumers/", + scopes: ["S1"], + account: account + }; + const params: kv = { }; + params[SSOTypes.SID] = account.sid; + setUtilUnifiedCacheQPStubs(params); + + accessTokenKey.authority = TEST_URIS.DEFAULT_INSTANCE + "9188040d-6c67-4c5b-b112-36a304b66dad/"; + cacheStorage.setItem(JSON.stringify(accessTokenKey), JSON.stringify(accessTokenValue)); + + accessTokenKey.authority = TEST_URIS.ALTERNATE_INSTANCE + "9188040d-6c67-4c5b-b112-36a304b66dad/"; + accessTokenValue.accessToken = "accessTokenAlternate"; + cacheStorage.setItem(JSON.stringify(accessTokenKey), JSON.stringify(accessTokenValue)); + + idTokenKey.authority = TEST_URIS.DEFAULT_INSTANCE + "9188040d-6c67-4c5b-b112-36a304b66dad/"; + cacheStorage.setItem(JSON.stringify(idTokenKey), JSON.stringify(idToken)); + idTokenKey.authority = TEST_URIS.ALTERNATE_INSTANCE + "9188040d-6c67-4c5b-b112-36a304b66dad/"; + cacheStorage.setItem(JSON.stringify(idTokenKey), JSON.stringify(idToken)); + + msal.acquireTokenSilent(tokenRequest).then(function(response) { + expect(response.scopes).to.be.deep.eq(["s1"]); + expect(response.account).to.be.eq(account); + expect(response.idToken.rawIdToken).to.eql(TEST_TOKENS.IDTOKEN_V2); + expect(response.idTokenClaims).to.eql(new IdToken(TEST_TOKENS.IDTOKEN_V2).claims); + expect(response.accessToken).to.include(TEST_TOKENS.ACCESSTOKEN); + expect(response.tokenType).to.be.eq(ServerHashParamKeys.ACCESS_TOKEN); + }).catch(function(err: AuthError) { + // Won't happen + console.error("Shouldn't have error here. Data: " + JSON.stringify(err)); + }); + msal.acquireTokenSilent(tokenRequestAlternate).then(function(response) { + expect(response.scopes).to.be.deep.eq(["s1"]); + expect(response.account).to.be.eq(account); + expect(response.idToken.rawIdToken).to.eql(TEST_TOKENS.IDTOKEN_V2); + expect(response.idTokenClaims).to.eql(new IdToken(TEST_TOKENS.IDTOKEN_V2).claims); + expect(response.accessToken).to.include("accessTokenAlternate"); + expect(response.tokenType).to.be.eq(ServerHashParamKeys.ACCESS_TOKEN); + done(); + }).catch(function(err: AuthError) { + // Won't happen + console.error("Shouldn't have error here. Data: " + JSON.stringify(err)); + }); + }); + it("tests getCachedToken when tenant authority is passed and single matching accessToken is found", function (done) { const tokenRequest : AuthenticationParameters = { diff --git a/lib/msal-core/test/error/ClientAuthError.spec.ts b/lib/msal-core/test/error/ClientAuthError.spec.ts index 44a086717f..b8f8de210f 100644 --- a/lib/msal-core/test/error/ClientAuthError.spec.ts +++ b/lib/msal-core/test/error/ClientAuthError.spec.ts @@ -48,27 +48,6 @@ describe("ClientAuthError.ts Class", () => { }); - it("createMultipleAuthoritiesInCacheError creates a ClientAuthError object", () => { - - const scope: string = "user.read"; - const errorDetail: string = "Cache error for scope"; - const multipleAuthoritiesError = ClientAuthError.createMultipleAuthoritiesInCacheError(scope); - let err: ClientAuthError; - - try { - throw multipleAuthoritiesError; - } catch (error) { - err = error; - } - - expect(err.errorCode).to.equal(ClientAuthErrorMessage.multipleCacheAuthorities.code); - expect(err.errorMessage).to.include(ClientAuthErrorMessage.multipleCacheAuthorities.desc); - expect(err.errorMessage).to.include(`${errorDetail} ${scope}`); - expect(err.message).to.include(ClientAuthErrorMessage.multipleCacheAuthorities.desc); - expect(err.name).to.equal("ClientAuthError"); - expect(err.stack).to.include("ClientAuthError.spec.ts"); - }); - it("createPopupWindowError creates a ClientAuthError object", () => { const ERROR_DETAIL = "Details:"; diff --git a/lib/msal-core/test/utils/UrlUtils.spec.ts b/lib/msal-core/test/utils/UrlUtils.spec.ts index 583ee56762..5220684fc5 100644 --- a/lib/msal-core/test/utils/UrlUtils.spec.ts +++ b/lib/msal-core/test/utils/UrlUtils.spec.ts @@ -34,12 +34,17 @@ describe("UrlUtils.ts class", () => { it("/organizations", () => { expect(UrlUtils.replaceTenantPath("http://a.com/organizations", "1234-5678")).to.eq("http://a.com/1234-5678/"); }); + + it("/consumers", () => { + expect(UrlUtils.replaceTenantPath("http://login.microsoftonline.com/consumers", "9188040d-6c67-4c5b-b112-36a304b66dad")).to.eq("http://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/"); + }); }); it("isCommonAuthority", () => { expect(UrlUtils.isCommonAuthority("https://login.microsoftonline.com/common/")).to.eq(true); expect(UrlUtils.isCommonAuthority("https://login.microsoftonline.com/common")).to.eq(true); expect(UrlUtils.isCommonAuthority("https://login.microsoftonline.com/organizations/")).to.eq(false); + expect(UrlUtils.isCommonAuthority("https://login.microsoftonline.com/consumers/")).to.eq(false); expect(UrlUtils.isCommonAuthority("https://login.microsoftonline.com/123456789/")).to.eq(false); }); @@ -47,9 +52,18 @@ describe("UrlUtils.ts class", () => { expect(UrlUtils.isOrganizationsAuthority("https://login.microsoftonline.com/organizations/")).to.eq(true); expect(UrlUtils.isOrganizationsAuthority("https://login.microsoftonline.com/organizations")).to.eq(true); expect(UrlUtils.isOrganizationsAuthority("https://login.microsoftonline.com/common/")).to.eq(false); + expect(UrlUtils.isOrganizationsAuthority("https://login.microsoftonline.com/consumers/")).to.eq(false); expect(UrlUtils.isOrganizationsAuthority("https://login.microsoftonline.com/123456789/")).to.eq(false); }); + it("isConsumersAuthority", () => { + expect(UrlUtils.isConsumersAuthority("https://login.microsoftonline.com/consumers/")).to.eq(true); + expect(UrlUtils.isConsumersAuthority("https://login.microsoftonline.com/consumers")).to.eq(true); + expect(UrlUtils.isConsumersAuthority("https://login.microsoftonline.com/common/")).to.eq(false); + expect(UrlUtils.isConsumersAuthority("https://login.microsoftonline.com/organizations/")).to.eq(false); + expect(UrlUtils.isConsumersAuthority("https://login.microsoftonline.com/123456789/")).to.eq(false); + }); + it("test getHashFromUrl returns hash from url if hash is single character", () => { const hash = UrlUtils.getHashFromUrl(TEST_URL_HASH_SINGLE_CHAR); diff --git a/samples/msal-core-samples/VanillaJSTestApp/app/get_access_token_interactively/graph.js b/samples/msal-core-samples/VanillaJSTestApp/app/get_access_token_interactively/graph.js index 4cb9ee3bd0..156c31a2d0 100644 --- a/samples/msal-core-samples/VanillaJSTestApp/app/get_access_token_interactively/graph.js +++ b/samples/msal-core-samples/VanillaJSTestApp/app/get_access_token_interactively/graph.js @@ -1,6 +1,6 @@ // Add here the endpoints for MS Graph API services you would like to use. const graphConfig = { - graphMeEndpoint: "https://graph.microsoft.com/v1.0/me" + graphMeEndpoint: "https://graph.microsoft-ppe.com/v1.0/me" }; // Helper function to call MS Graph API endpoint