From 751e5f19d2e6fac184a0a17cc216c54cc5c1e9f2 Mon Sep 17 00:00:00 2001 From: Patrick Arlt Date: Mon, 25 Sep 2017 06:54:27 -0700 Subject: [PATCH] feat(UserSession): multiple requests to getToken for similar URLs now return the same Promise When called many times in quick succession getToken would make multiple requests to token endpoints. Now only 1 request is made and the existing Promise is returned. AFFECTS PACKAGES: @esri/arcgis-rest-auth --- packages/arcgis-rest-auth/src/UserSession.ts | 23 ++++- .../arcgis-rest-auth/test/UserSession.test.ts | 99 +++++++++++++++++++ 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/packages/arcgis-rest-auth/src/UserSession.ts b/packages/arcgis-rest-auth/src/UserSession.ts index fa06140616..b89ef41484 100644 --- a/packages/arcgis-rest-auth/src/UserSession.ts +++ b/packages/arcgis-rest-auth/src/UserSession.ts @@ -176,6 +176,9 @@ export class UserSession implements IAuthenticationManager { private _tokenExpires: Date; private _refreshToken: string; private _refreshTokenExpires: Date; + private _pendingTokenRequests: { + [key: string]: Promise; + }; /** * Internal list of trusted 3rd party servers (federated servers) that have @@ -229,6 +232,7 @@ export class UserSession implements IAuthenticationManager { this.redirectUri = options.redirectUri; this.refreshTokenDuration = options.refreshTokenDuration || 20160; this.trustedServers = {}; + this._pendingTokenRequests = {}; } /** @@ -504,7 +508,11 @@ export class UserSession implements IAuthenticationManager { return Promise.resolve(existingToken.token); } - return request(`${root}/rest/info`) + if (this._pendingTokenRequests[root]) { + return this._pendingTokenRequests[root]; + } + + this._pendingTokenRequests[root] = request(`${root}/rest/info`) .then((response: any) => { return response.owningSystemUrl; }) @@ -538,6 +546,8 @@ export class UserSession implements IAuthenticationManager { }; return response.token; }); + + return this._pendingTokenRequests[root]; } /** @@ -552,7 +562,16 @@ export class UserSession implements IAuthenticationManager { return Promise.resolve(this.token); } - return this.refreshSession().then(session => session.token); + if (!this._pendingTokenRequests[this.portal]) { + this._pendingTokenRequests[ + this.portal + ] = this.refreshSession().then(session => { + this._pendingTokenRequests[this.portal] = null; + return session.token; + }); + } + + return this._pendingTokenRequests[this.portal]; } /** diff --git a/packages/arcgis-rest-auth/test/UserSession.test.ts b/packages/arcgis-rest-auth/test/UserSession.test.ts index ba311a6454..4d3337d1ba 100644 --- a/packages/arcgis-rest-auth/test/UserSession.test.ts +++ b/packages/arcgis-rest-auth/test/UserSession.test.ts @@ -4,6 +4,8 @@ import * as fetchMock from "fetch-mock"; import { YESTERDAY, TOMORROW } from "./utils"; describe("UserSession", () => { + afterEach(fetchMock.restore); + it("should serialze to and from JSON", () => { const session = new UserSession({ clientId: "clientId", @@ -155,6 +157,67 @@ describe("UserSession", () => { }); }); + it("should only make 1 token request to an untrusted portal for similar URLs", done => { + const session = new UserSession({ + clientId: "id", + token: "token", + refreshToken: "refresh", + tokenExpires: TOMORROW, + portal: "https://gis.city.gov/sharing/rest" + }); + + fetchMock.mock( + "https://gisservices.city.gov/public/rest/info", + { + currentVersion: 10.51, + fullVersion: "10.5.1.120", + owningSystemUrl: "https://gis.city.gov", + authInfo: { + isTokenBasedSecurity: true, + tokenServicesUrl: "https://gis.city.gov/sharing/generateToken" + } + }, + { times: 1, method: "POST" } + ); + + fetchMock.mock( + "https://gis.city.gov/sharing/rest/info", + { + owningSystemUrl: "http://gis.city.gov", + authInfo: { + tokenServicesUrl: "https://gis.city.gov/sharing/generateToken", + isTokenBasedSecurity: true + } + }, + { times: 1, method: "POST" } + ); + + fetchMock.mock( + "https://gis.city.gov/sharing/generateToken", + { + token: "serverToken", + expires: TOMORROW + }, + { times: 1, method: "POST" } + ); + + Promise.all([ + session.getToken( + "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query" + ), + session.getToken( + "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query" + ) + ]).then(([token1, token2]) => { + expect(token1).toBe("serverToken"); + expect(token2).toBe("serverToken"); + expect( + fetchMock.calls("https://gis.city.gov/sharing/generateToken").length + ).toBe(1); + done(); + }); + }); + it("should throw an ArcGISAuthError when the owning system doesn't match", done => { const session = new UserSession({ clientId: "id", @@ -291,6 +354,42 @@ describe("UserSession", () => { done(); }); }); + + it("should only make 1 token request to the portal for similar URLs", done => { + const session = new UserSession({ + clientId: "id", + token: "token", + refreshToken: "refresh", + tokenExpires: YESTERDAY + }); + + fetchMock.mock( + "https://www.arcgis.com/sharing/rest/oauth2/token", + { + access_token: "new", + expires_in: 1800, + username: "casey" + }, + { times: 1, method: "POST" } + ); + + Promise.all([ + session.getToken("https://www.arcgis.com/sharing/rest/portals/self"), + session.getToken("https://www.arcgis.com/sharing/rest/portals/self") + ]) + .then(([token1, token2]) => { + expect(token1).toBe("new"); + expect(token2).toBe("new"); + expect( + fetchMock.calls("https://www.arcgis.com/sharing/rest/oauth2/token") + .length + ).toBe(1); + done(); + }) + .catch(e => { + fail(e); + }); + }); }); describe(".beginOAuth2()", () => {