From 06e98c050f0010c37affe67e76f496603d861409 Mon Sep 17 00:00:00 2001 From: Patrick Arlt Date: Thu, 29 Apr 2021 11:21:48 -0700 Subject: [PATCH 1/6] Add support for sending all credentials to a list of trusted servers/domains from portal --- package-lock.json | 34 ++- packages/arcgis-rest-auth/src/UserSession.ts | 262 +++++++++++------- packages/arcgis-rest-request/src/request.ts | 6 +- .../src/utils/IAuthenticationManager.ts | 1 + .../src/utils/IRequestOptions.ts | 4 + 5 files changed, 200 insertions(+), 107 deletions(-) diff --git a/package-lock.json b/package-lock.json index 90682f1097..e11f4ceaf7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2151,6 +2151,16 @@ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", @@ -3152,6 +3162,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -4903,6 +4914,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -6581,6 +6593,13 @@ "escape-string-regexp": "^1.0.5" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -7900,7 +7919,7 @@ "has-binary2": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "integrity": "sha1-d3asYn8+p3JQz8My2rfd9eT10R0=", "dev": true, "requires": { "isarray": "2.0.1" @@ -9169,7 +9188,7 @@ "is-number-like": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", - "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", + "integrity": "sha1-LhKWILUIkQQuROm7uzBZPnXPu+M=", "dev": true, "requires": { "lodash.isfinite": "^3.3.2" @@ -12736,6 +12755,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -14836,7 +14856,7 @@ "send": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "integrity": "sha1-bsyh4PjBVtFBWXVZhI32RzCmu8E=", "dev": true, "requires": { "debug": "2.6.9", @@ -14884,7 +14904,7 @@ "mime": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "integrity": "sha1-Eh+evEnjdm8xGnbh+hyAA8SwOqY=", "dev": true }, "setprototypeof": { @@ -14896,7 +14916,7 @@ "statuses": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic=", "dev": true } } @@ -14966,7 +14986,7 @@ "serve-static": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "integrity": "sha1-CV6Ecv1bRiN9tQzkhqQ/S4bGzsE=", "dev": true, "requires": { "encodeurl": "~1.0.2", @@ -17125,7 +17145,7 @@ "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "integrity": "sha1-YQY29rH3A4kb00dxzLF/uTtHB5w=", "dev": true }, "wordwrap": { diff --git a/packages/arcgis-rest-auth/src/UserSession.ts b/packages/arcgis-rest-auth/src/UserSession.ts index eeb2b468d2..afa905ddc2 100644 --- a/packages/arcgis-rest-auth/src/UserSession.ts +++ b/packages/arcgis-rest-auth/src/UserSession.ts @@ -74,21 +74,6 @@ function defer(): IDeferred { return deferred as IDeferred; } -/** - * Used to test if a URL is an ArcGIS Online URL - */ -const arcgisOnlineUrlRegex = /^https?:\/\/\S+\.arcgis\.com.+/; - -/** - * Used to test if a URL is production ArcGIS Online Portal - */ -const arcgisOnlinePortalRegex = /^https?:\/\/www\.arcgis\.com\/sharing\/rest+/; - -/** - * Used to test if a URL is an ArcGIS Online Organization Portal - */ -const arcgisOnlineOrgPortalRegex = /^https?:\/\/(?:[a-z0-9-]+\.maps)?.\arcgis\.com\/sharing\/rest/; - /** * Options for static OAuth 2.0 helper methods on `UserSession`. */ @@ -318,7 +303,8 @@ export class UserSession implements IAuthenticationManager { provider: "arcgis", duration: 20160, popup: true, - popupWindowFeatures: "height=400,width=600,menubar=no,location=yes,resizable=yes,scrollbars=yes,status=yes", + popupWindowFeatures: + "height=400,width=600,menubar=no,location=yes,resizable=yes,scrollbars=yes,status=yes", state: options.clientId, locale: "", }, @@ -372,11 +358,7 @@ export class UserSession implements IAuthenticationManager { } }; - win.open( - url, - "oauth-window", - popupWindowFeatures - ); + win.open(url, "oauth-window", popupWindowFeatures); return session.promise; } @@ -709,6 +691,7 @@ export class UserSession implements IAuthenticationManager { private _refreshToken: string; private _refreshTokenExpires: Date; private _pendingUserRequest: Promise; + private _pendingPortalRequest: Promise; /** * Internal object to keep track of pending token requests. Used to prevent @@ -719,16 +702,22 @@ export class UserSession implements IAuthenticationManager { }; /** - * Internal list of trusted 3rd party servers (federated servers) that have - * been validated with `generateToken`. + * Internal list of tokens to 3rd party servers (federated servers) that have + * been created via `generateToken`. The object key is the root URL of the server. */ - private trustedServers: { + private federatedServers: { [key: string]: { token: string; expires: Date; }; }; + /** + * Internal list of 3rd party domains that should receive all cookies (credentials: "include"). + * Used to for PKI and IWA workflows in high security environments. + */ + private trustedDomains: string[]; + private _hostHandler: any; constructor(options: IUserSessionOptions) { @@ -748,13 +737,14 @@ export class UserSession implements IAuthenticationManager { this.redirectUri = options.redirectUri; this.refreshTokenTTL = options.refreshTokenTTL || 1440; - this.trustedServers = {}; + this.federatedServers = {}; + this.trustedDomains = []; // if a non-federated server was passed explicitly, it should be trusted. if (options.server) { // if the url includes more than '/arcgis/', trim the rest const root = this.getServerRootUrl(options.server); - this.trustedServers[root] = { + this.federatedServers[root] = { token: options.token, expires: options.tokenExpires, }; @@ -819,6 +809,44 @@ export class UserSession implements IAuthenticationManager { } } + /** + * Returns information about the currently logged in [user](https://developers.arcgis.com/rest/users-groups-and-items/user.htm). Subsequent calls will *not* result in additional web traffic. + * + * ```js + * session.getUser() + * .then(response => { + * console.log(response.role); // "org_admin" + * }) + * ``` + * + * @param requestOptions - Options for the request. NOTE: `rawResponse` is not supported by this operation. + * @returns A Promise that will resolve with the data from the response. + */ + public getPortal(requestOptions?: IRequestOptions): Promise { + if (this._pendingPortalRequest) { + return this._pendingPortalRequest; + } else if (this._user) { + return Promise.resolve(this._user); + } else { + const url = `${this.portal}/portals/self`; + + const options = { + httpMethod: "GET", + authentication: this, + ...requestOptions, + rawResponse: false, + } as IRequestOptions; + + this._pendingPortalRequest = request(url, options).then((response) => { + this._user = response; + this._pendingPortalRequest = null; + return response; + }); + + return this._pendingPortalRequest; + } + } + /** * Returns the username for the currently logged in [user](https://developers.arcgis.com/rest/users-groups-and-items/user.htm). Subsequent calls will *not* result in additional web traffic. This is also used internally when a username is required for some requests but is not present in the options. * @@ -992,7 +1020,7 @@ export class UserSession implements IAuthenticationManager { /** * Validates that a given URL is properly federated with our current `portal`. - * Attempts to use the internal `trustedServers` cache first. + * Attempts to use the internal `federatedServers` cache first. */ private getTokenForServer( url: string, @@ -1001,7 +1029,7 @@ export class UserSession implements IAuthenticationManager { // requests to /rest/services/ and /rest/admin/services/ are both valid // Federated servers may have inconsistent casing, so lowerCase it const root = this.getServerRootUrl(url); - const existingToken = this.trustedServers[root]; + const existingToken = this.federatedServers[root]; if ( existingToken && @@ -1015,82 +1043,106 @@ export class UserSession implements IAuthenticationManager { return this._pendingTokenRequests[root]; } - this._pendingTokenRequests[root] = request(`${root}/rest/info`) - .then((response) => { - if (response.owningSystemUrl) { - /** - * if this server is not owned by this portal - * bail out with an error since we know we wont - * be able to generate a token - */ - if (!isFederated(response.owningSystemUrl, this.portal)) { + this._pendingTokenRequests[root] = this.getPortal().then((portalInfo) => { + /** + * Domains can be configured as secure.esri.com or https://secure.esri.com this normalizes to https://secure.esri.com so we can use startsWith later. + */ + if ( + portalInfo.authorizedCrossOriginDomains && + portalInfo.authorizedCrossOriginDomains.length + ) { + this.trustedDomains = portalInfo.authorizedCrossOriginDomains + .filter((d: string) => !d.startsWith("http://")) + .map((d: string) => { + if (d.startsWith("https://")) { + return d; + } else { + return `https://${d}`; + } + }); + } + + return request(`${root}/rest/info`, { + credentials: this.getDomainCredentials(url), + }) + .then((response) => { + if (response.owningSystemUrl) { + /** + * if this server is not owned by this portal + * bail out with an error since we know we wont + * be able to generate a token + */ + if (!isFederated(response.owningSystemUrl, this.portal)) { + throw new ArcGISAuthError( + `${url} is not federated with ${this.portal}.`, + "NOT_FEDERATED" + ); + } else { + /** + * if the server is federated, use the relevant token endpoint. + */ + return request( + `${response.owningSystemUrl}/sharing/rest/info`, + requestOptions + ); + } + } else if ( + response.authInfo && + this.federatedServers[root] !== undefined + ) { + /** + * if its a stand-alone instance of ArcGIS Server that doesn't advertise + * federation, but the root server url is recognized, use its built in token endpoint. + */ + return Promise.resolve({ + authInfo: response.authInfo, + }); + } else { throw new ArcGISAuthError( - `${url} is not federated with ${this.portal}.`, + `${url} is not federated with any portal and is not explicitly trusted.`, "NOT_FEDERATED" ); + } + }) + .then((response: any) => { + return response.authInfo.tokenServicesUrl; + }) + .then((tokenServicesUrl: string) => { + // an expired token cant be used to generate a new token + if (this.token && this.tokenExpires.getTime() > Date.now()) { + return generateToken(tokenServicesUrl, { + params: { + token: this.token, + serverUrl: url, + expiration: this.tokenDuration, + client: "referer", + }, + }); + // generate an entirely fresh token if necessary } else { - /** - * if the server is federated, use the relevant token endpoint. - */ - return request( - `${response.owningSystemUrl}/sharing/rest/info`, - requestOptions - ); + return generateToken(tokenServicesUrl, { + params: { + username: this.username, + password: this.password, + expiration: this.tokenDuration, + client: "referer", + }, + }).then((response: any) => { + this._token = response.token; + this._tokenExpires = new Date(response.expires); + return response; + }); } - } else if ( - response.authInfo && - this.trustedServers[root] !== undefined - ) { - /** - * if its a stand-alone instance of ArcGIS Server that doesn't advertise - * federation, but the root server url is recognized, use its built in token endpoint. - */ - return Promise.resolve({ authInfo: response.authInfo }); - } else { - throw new ArcGISAuthError( - `${url} is not federated with any portal and is not explicitly trusted.`, - "NOT_FEDERATED" - ); - } - }) - .then((response: any) => { - return response.authInfo.tokenServicesUrl; - }) - .then((tokenServicesUrl: string) => { - // an expired token cant be used to generate a new token - if (this.token && this.tokenExpires.getTime() > Date.now()) { - return generateToken(tokenServicesUrl, { - params: { - token: this.token, - serverUrl: url, - expiration: this.tokenDuration, - client: "referer", - }, - }); - // generate an entirely fresh token if necessary - } else { - return generateToken(tokenServicesUrl, { - params: { - username: this.username, - password: this.password, - expiration: this.tokenDuration, - client: "referer", - }, - }).then((response: any) => { - this._token = response.token; - this._tokenExpires = new Date(response.expires); - return response; - }); - } - }) - .then((response) => { - this.trustedServers[root] = { - expires: new Date(response.expires), - token: response.token, - }; - delete this._pendingTokenRequests[root]; - return response.token; - }); + }) + .then((response) => { + this.federatedServers[root] = { + expires: new Date(response.expires), + token: response.token, + }; + delete this._pendingTokenRequests[root]; + return response.token; + }); + }); return this._pendingTokenRequests[root]; } @@ -1203,4 +1255,16 @@ export class UserSession implements IAuthenticationManager { } ); } + + getDomainCredentials(url: string): RequestCredentials { + if (!this.trustedDomains || !this.trustedDomains.length) { + return "same-origin"; + } + + return this.trustedDomains.some((domainWithProtocol) => { + return url.startsWith(domainWithProtocol); + }) + ? "include" + : "same-origin"; + } } diff --git a/packages/arcgis-rest-request/src/request.ts b/packages/arcgis-rest-request/src/request.ts index da94dbc7bd..32a977513e 100644 --- a/packages/arcgis-rest-request/src/request.ts +++ b/packages/arcgis-rest-request/src/request.ts @@ -254,7 +254,7 @@ export function request( method: httpMethod, /* ensures behavior mimics XMLHttpRequest. needed to support sending IWA cookies */ - credentials: "same-origin", + credentials: options.credentials || "same-origin", }; // the /oauth2/platformSelf route will add X-Esri-Auth-Client-Id header @@ -292,6 +292,10 @@ export function request( params.token = token; } + if (authentication && authentication.getDomainCredentials) { + fetchOptions.credentials = authentication.getDomainCredentials(url); + } + // Custom headers to add to request. IRequestOptions.headers with merge over requestHeaders. const requestHeaders: { [key: string]: any; diff --git a/packages/arcgis-rest-request/src/utils/IAuthenticationManager.ts b/packages/arcgis-rest-request/src/utils/IAuthenticationManager.ts index 9c190840fb..de62c91353 100644 --- a/packages/arcgis-rest-request/src/utils/IAuthenticationManager.ts +++ b/packages/arcgis-rest-request/src/utils/IAuthenticationManager.ts @@ -18,4 +18,5 @@ export interface IAuthenticationManager { */ portal: string; getToken(url: string, requestOptions?: ITokenRequestOptions): Promise; + getDomainCredentials?(url: string): RequestCredentials; } diff --git a/packages/arcgis-rest-request/src/utils/IRequestOptions.ts b/packages/arcgis-rest-request/src/utils/IRequestOptions.ts index b1d8e96d2f..1f29de5c1b 100644 --- a/packages/arcgis-rest-request/src/utils/IRequestOptions.ts +++ b/packages/arcgis-rest-request/src/utils/IRequestOptions.ts @@ -36,6 +36,10 @@ export interface IRequestOptions { * The implementation of `fetch` to use. Defaults to a global `fetch`. */ fetch?: (input: RequestInfo, init?: RequestInit) => Promise; + /** + * A string indicating whether credentials (cookies) will be sent with the request. Used internally for authentication workflows. + */ + credentials?: RequestCredentials; /** * If the length of a GET request's URL exceeds `maxUrlLength` the request will use POST instead. */ From bf2c5c68a923a87e821bbb7a18859dcf14eac5c1 Mon Sep 17 00:00:00 2001 From: Patrick Arlt Date: Fri, 30 Apr 2021 08:28:06 -0700 Subject: [PATCH 2/6] small fixes --- packages/arcgis-rest-auth/src/UserSession.ts | 62 ++++++++----- .../test/addTo.test.ts | 86 +++++++++---------- 2 files changed, 82 insertions(+), 66 deletions(-) diff --git a/packages/arcgis-rest-auth/src/UserSession.ts b/packages/arcgis-rest-auth/src/UserSession.ts index 62f8409fcd..0d6613ee6d 100644 --- a/packages/arcgis-rest-auth/src/UserSession.ts +++ b/packages/arcgis-rest-auth/src/UserSession.ts @@ -607,8 +607,8 @@ export class UserSession implements IAuthenticationManager { public static fromCredential(credential: ICredential) { // At ArcGIS Online 9.1, credentials no longer include the ssl and expires properties // Here, we provide default values for them to cover this condition - const ssl = (typeof credential.ssl !== "undefined") ? credential.ssl : true; - const expires = credential.expires || (Date.now() + 7200000 /* 2 hours */); + const ssl = typeof credential.ssl !== "undefined" ? credential.ssl : true; + const expires = credential.expires || Date.now() + 7200000 /* 2 hours */; return new UserSession({ portal: credential.server.includes("sharing/rest") @@ -617,7 +617,7 @@ export class UserSession implements IAuthenticationManager { ssl, token: credential.token, username: credential.userId, - tokenExpires: new Date(expires) + tokenExpires: new Date(expires), }); } @@ -700,6 +700,11 @@ export class UserSession implements IAuthenticationManager { */ private _user: IUser; + /** + * Hydrated by a call to [getPortal()](#getPortal-summary). + */ + private _portal: any; + private _token: string; private _tokenExpires: Date; private _refreshToken: string; @@ -753,6 +758,7 @@ export class UserSession implements IAuthenticationManager { this.federatedServers = {}; this.trustedDomains = []; + // if a non-federated server was passed explicitly, it should be trusted. if (options.server) { // if the url includes more than '/arcgis/', trim the rest @@ -824,12 +830,12 @@ export class UserSession implements IAuthenticationManager { } /** - * Returns information about the currently logged in [user](https://developers.arcgis.com/rest/users-groups-and-items/user.htm). Subsequent calls will *not* result in additional web traffic. + * Returns information about the currently logged in users [portal](https://developers.arcgis.com/rest/users-groups-and-items/portal-self.htm). Subsequent calls will *not* result in additional web traffic. * * ```js - * session.getUser() + * session.getPortal() * .then(response => { - * console.log(response.role); // "org_admin" + * console.log(portal.name); // "City of ..." * }) * ``` * @@ -839,8 +845,8 @@ export class UserSession implements IAuthenticationManager { public getPortal(requestOptions?: IRequestOptions): Promise { if (this._pendingPortalRequest) { return this._pendingPortalRequest; - } else if (this._user) { - return Promise.resolve(this._user); + } else if (this._portal) { + return Promise.resolve(this._portal); } else { const url = `${this.portal}/portals/self`; @@ -852,7 +858,7 @@ export class UserSession implements IAuthenticationManager { } as IRequestOptions; this._pendingPortalRequest = request(url, options).then((response) => { - this._user = response; + this._portal = response; this._pendingPortalRequest = null; return response; }); @@ -999,6 +1005,27 @@ export class UserSession implements IAuthenticationManager { // in the path which cannot be lowercased. return `${protocol}${domain.toLowerCase()}/${path.join("/")}`; } + + /** + * Returns the proper [`credentials`] option for `fetch` for a given domain. + * See [trusted server](https://enterprise.arcgis.com/en/portal/latest/administer/windows/configure-security.htm#ESRI_SECTION1_70CC159B3540440AB325BE5D89DBE94A). + * Used internally by underlying request methods to add support for specific security considerations. + * + * @param url The url of the request + * @returns "include" or "same-origin" + */ + public getDomainCredentials(url: string): RequestCredentials { + if (!this.trustedDomains || !this.trustedDomains.length) { + return "same-origin"; + } + + return this.trustedDomains.some((domainWithProtocol) => { + return url.startsWith(domainWithProtocol); + }) + ? "include" + : "same-origin"; + } + /** * Return a function that closes over the validOrigins array and * can be used as an event handler for the `message` event @@ -1028,7 +1055,7 @@ export class UserSession implements IAuthenticationManager { const credential = this.toCredential(); // the following line allows us to conform to our spec without changing other depended-on functionality // https://github.com/Esri/arcgis-rest-js/blob/master/packages/arcgis-rest-auth/post-message-auth-spec.md#arcgisauthcredential - credential.server = credential.server.replace('/sharing/rest', ''); + credential.server = credential.server.replace("/sharing/rest", ""); event.source.postMessage( { type: "arcgis:auth:credential", @@ -1067,7 +1094,8 @@ export class UserSession implements IAuthenticationManager { this._pendingTokenRequests[root] = this.getPortal().then((portalInfo) => { /** - * Domains can be configured as secure.esri.com or https://secure.esri.com this normalizes to https://secure.esri.com so we can use startsWith later. + * Specific domains can be configured as secure.esri.com or https://secure.esri.com this + * normalizes to https://secure.esri.com so we can use startsWith later. */ if ( portalInfo.authorizedCrossOriginDomains && @@ -1277,16 +1305,4 @@ export class UserSession implements IAuthenticationManager { } ); } - - getDomainCredentials(url: string): RequestCredentials { - if (!this.trustedDomains || !this.trustedDomains.length) { - return "same-origin"; - } - - return this.trustedDomains.some((domainWithProtocol) => { - return url.startsWith(domainWithProtocol); - }) - ? "include" - : "same-origin"; - } } diff --git a/packages/arcgis-rest-service-admin/test/addTo.test.ts b/packages/arcgis-rest-service-admin/test/addTo.test.ts index 7547a49e9f..dcfc694a72 100644 --- a/packages/arcgis-rest-service-admin/test/addTo.test.ts +++ b/packages/arcgis-rest-service-admin/test/addTo.test.ts @@ -12,7 +12,7 @@ import { AddToFeatureServiceSuccessResponseFredAndGinger, AddToFeatureServiceSuccessResponseFayardAndHarold, AddToFeatureServiceSuccessResponseCydAndGene, - AddToFeatureServiceError + AddToFeatureServiceError, } from "./mocks/service"; import { layerDefinitionSid } from "./mocks/layerDefinition"; @@ -31,69 +31,69 @@ describe("add to feature service", () => { refreshTokenTTL: 1440, username: "casey", password: "123456", - portal: "https://myorg.maps.arcgis.com/sharing/rest" + portal: "https://myorg.maps.arcgis.com/sharing/rest", }); const MOCK_USER_REQOPTS = { - authentication: MOCK_USER_SESSION + authentication: MOCK_USER_SESSION, }; const layerDescriptionFred: ILayer = { name: "Fred", id: "1899", - layerType: "Feature Layer" + layerType: "Feature Layer", }; const layerDescriptionGinger: ILayer = { name: "Ginger", id: "1911", - layerType: "Feature Layer" + layerType: "Feature Layer", }; const layerDescriptionCyd: ILayer = { name: "Cyd", id: "1922", - layerType: "Feature Layer" + layerType: "Feature Layer", }; const layerDescriptionFail: ILayer = { name: "", id: "", - layerType: "Feature Layer" + layerType: "Feature Layer", }; const tableDescriptionFayard: ITable = { name: "Fayard", - id: 1914 + id: "1914", }; const tableDescriptionHarold: ITable = { name: "Harold", - id: 1921 + id: "1921", }; const tableDescriptionGene: ITable = { name: "Gene", - id: 1912 + id: "1912", }; const tableDescriptionFail: ITable = { name: "", - id: 0 + id: "0", }; - it("should add a pair of layers", done => { + it("should add a pair of layers", (done) => { fetchMock.once("*", AddToFeatureServiceSuccessResponseFredAndGinger); addToServiceDefinition( "https://services1.arcgis.com/ORG/arcgis/rest/services/FEATURE_SERVICE/FeatureServer", { layers: [layerDescriptionFred, layerDescriptionGinger], - ...MOCK_USER_REQOPTS + ...MOCK_USER_REQOPTS, } ) .then( - response => { + (response) => { // Check service call expect(fetchMock.called()).toEqual(true); const [url, options]: [string, RequestInit] = fetchMock.lastCall( @@ -110,7 +110,7 @@ describe("add to feature service", () => { encodeParam( "addToDefinition", JSON.stringify({ - layers: [layerDescriptionFred, layerDescriptionGinger] + layers: [layerDescriptionFred, layerDescriptionGinger], }) ) ); @@ -126,23 +126,23 @@ describe("add to feature service", () => { fail(); // call is supposed to succeed } ) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should add a pair of tables", done => { + it("should add a pair of tables", (done) => { fetchMock.once("*", AddToFeatureServiceSuccessResponseFayardAndHarold); addToServiceDefinition( "https://services1.arcgis.com/ORG/arcgis/rest/services/FEATURE_SERVICE/FeatureServer", { tables: [tableDescriptionFayard, tableDescriptionHarold], - ...MOCK_USER_REQOPTS + ...MOCK_USER_REQOPTS, } ) .then( - response => { + (response) => { // Check service call expect(fetchMock.called()).toEqual(true); const [url, options]: [string, RequestInit] = fetchMock.lastCall( @@ -159,7 +159,7 @@ describe("add to feature service", () => { encodeParam( "addToDefinition", JSON.stringify({ - tables: [tableDescriptionFayard, tableDescriptionHarold] + tables: [tableDescriptionFayard, tableDescriptionHarold], }) ) ); @@ -175,12 +175,12 @@ describe("add to feature service", () => { fail(); // call is supposed to succeed } ) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should add a layer and a table", done => { + it("should add a layer and a table", (done) => { fetchMock.once("*", AddToFeatureServiceSuccessResponseCydAndGene); addToServiceDefinition( @@ -188,10 +188,10 @@ describe("add to feature service", () => { { layers: [layerDescriptionCyd], tables: [tableDescriptionGene], - ...MOCK_USER_REQOPTS + ...MOCK_USER_REQOPTS, } ) - .then(response => { + .then((response) => { // Check service call expect(fetchMock.called()).toEqual(true); const [url, options]: [string, RequestInit] = fetchMock.lastCall("*"); @@ -207,7 +207,7 @@ describe("add to feature service", () => { "addToDefinition", JSON.stringify({ layers: [layerDescriptionCyd], - tables: [tableDescriptionGene] + tables: [tableDescriptionGene], }) ) ); @@ -218,22 +218,22 @@ describe("add to feature service", () => { ); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should add a layer definition", done => { + it("should add a layer definition", (done) => { fetchMock.once("*", AddToFeatureServiceSuccessResponseCydAndGene); addToServiceDefinition( "https://services1.arcgis.com/ORG/arcgis/rest/services/FEATURE_SERVICE/FeatureServer", { layers: [layerDefinitionSid], - ...MOCK_USER_REQOPTS + ...MOCK_USER_REQOPTS, } ) - .then(response => { + .then((response) => { // Check service call expect(fetchMock.called()).toEqual(true); const [url, options]: [string, RequestInit] = fetchMock.lastCall("*"); @@ -248,7 +248,7 @@ describe("add to feature service", () => { encodeParam( "addToDefinition", JSON.stringify({ - layers: [layerDefinitionSid] + layers: [layerDefinitionSid], }) ) ); @@ -259,21 +259,21 @@ describe("add to feature service", () => { ); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should fail to add a bad layer", done => { + it("should fail to add a bad layer", (done) => { fetchMock.once("*", AddToFeatureServiceError); addToServiceDefinition( "https://services1.arcgis.com/ORG/arcgis/rest/services/FEATURE_SERVICE/FeatureServer", { layers: [layerDescriptionFail], - ...MOCK_USER_REQOPTS + ...MOCK_USER_REQOPTS, } - ).catch(error => { + ).catch((error) => { expect(error.name).toBe(ErrorTypes.ArcGISRequestError); expect(error.message).toBe( "400: Unable to add feature service definition." @@ -284,23 +284,23 @@ describe("add to feature service", () => { ); // params added internally aren't surfaced in the error expect(error.options.params.addToDefinition).toEqual({ - layers: [layerDescriptionFail] + layers: [layerDescriptionFail], }); expect(error.options.httpMethod).toEqual("POST"); done(); }); }); - it("should fail to add a bad table", done => { + it("should fail to add a bad table", (done) => { fetchMock.once("*", AddToFeatureServiceError); addToServiceDefinition( "https://services1.arcgis.com/ORG/arcgis/rest/services/FEATURE_SERVICE/FeatureServer", { tables: [tableDescriptionFail], - ...MOCK_USER_REQOPTS + ...MOCK_USER_REQOPTS, } - ).catch(error => { + ).catch((error) => { expect(error.name).toBe(ErrorTypes.ArcGISRequestError); expect(error.message).toBe( "400: Unable to add feature service definition." @@ -311,14 +311,14 @@ describe("add to feature service", () => { ); // params added internally aren't surfaced in the error expect(error.options.params.addToDefinition).toEqual({ - tables: [tableDescriptionFail] + tables: [tableDescriptionFail], }); expect(error.options.httpMethod).toEqual("POST"); done(); }); }); - it("should fail to add a bad layer and a bad table", done => { + it("should fail to add a bad layer and a bad table", (done) => { fetchMock.once("*", AddToFeatureServiceError); addToServiceDefinition( @@ -326,9 +326,9 @@ describe("add to feature service", () => { { layers: [layerDescriptionFail], tables: [tableDescriptionFail], - ...MOCK_USER_REQOPTS + ...MOCK_USER_REQOPTS, } - ).catch(error => { + ).catch((error) => { expect(error.name).toBe(ErrorTypes.ArcGISRequestError); expect(error.message).toBe( "400: Unable to add feature service definition." @@ -340,7 +340,7 @@ describe("add to feature service", () => { // params added internally aren't surfaced in the error expect(error.options.params.addToDefinition).toEqual({ tables: [tableDescriptionFail], - layers: [layerDescriptionFail] + layers: [layerDescriptionFail], }); expect(error.options.httpMethod).toEqual("POST"); done(); From 083a0edc7a11853d20900e082f15e7c321bd3d9a Mon Sep 17 00:00:00 2001 From: Patrick Arlt Date: Mon, 3 May 2021 16:45:45 -0700 Subject: [PATCH 3/6] add and fix tests --- docs/src/guides/browser-authentication.md | 20 +- packages/arcgis-rest-auth/src/UserSession.ts | 210 +++++---- .../arcgis-rest-auth/src/generate-token.ts | 2 +- .../arcgis-rest-auth/test/UserSession.test.ts | 406 +++++++++++++++++- packages/arcgis-rest-request/src/request.ts | 4 +- .../test/addTo.test.ts | 8 +- 6 files changed, 537 insertions(+), 113 deletions(-) diff --git a/docs/src/guides/browser-authentication.md b/docs/src/guides/browser-authentication.md index 2a82f80f00..e63230bc37 100644 --- a/docs/src/guides/browser-authentication.md +++ b/docs/src/guides/browser-authentication.md @@ -12,30 +12,28 @@ In the [Node.js](/arcgis-rest-js/guides/node/) guide we explained how to instant ![browser based login](https://developers.arcgis.com/documentation/core-concepts/security-and-authentication/images/authorization-screen.png) - ### Resources -* [Implementing Named User Login](https://developers.arcgis.com/documentation/core-concepts/security-and-authentication/signing-in-arcgis-online-users/) -* [Browser-based Named User Login](https://developers.arcgis.com/documentation/core-concepts/security-and-authentication/browser-based-user-logins/) +- [Implementing Named User Login](https://developers.arcgis.com/documentation/core-concepts/security-and-authentication/signing-in-arcgis-online-users/) +- [Browser-based Named User Login](https://developers.arcgis.com/documentation/core-concepts/security-and-authentication/browser-based-user-logins/) ```js // register your own app to create a unique clientId -const clientId = "abc123" +const clientId = "abc123"; UserSession.beginOAuth2({ clientId, - redirectUri: 'https://yourapp.com/authenticate.html' -}) - .then(session) + redirectUri: "https://yourapp.com/authenticate.html", +}).then(session); ``` -After the user has logged in, the `session` will keep track of individual `trustedServers` that are known to be federated and pass a token through when making requests. +After the user has logged in, the `session` will keep track of individual `federatedServers` that are known to be federated and pass a token through when making requests. ```js -request(url, { authentication: session }) +request(url, { authentication: session }); ``` ### Demos -* [OAuth 2.0 Browser](https://github.com/Esri/arcgis-rest-js/tree/master/demos/oauth2-browser) -* [Retrying Requests](https://github.com/Esri/arcgis-rest-js/tree/master/demos/oauth2-browser-retry) +- [OAuth 2.0 Browser](https://github.com/Esri/arcgis-rest-js/tree/master/demos/oauth2-browser) +- [Retrying Requests](https://github.com/Esri/arcgis-rest-js/tree/master/demos/oauth2-browser-retry) diff --git a/packages/arcgis-rest-auth/src/UserSession.ts b/packages/arcgis-rest-auth/src/UserSession.ts index 0d6613ee6d..2c7f9a5733 100644 --- a/packages/arcgis-rest-auth/src/UserSession.ts +++ b/packages/arcgis-rest-auth/src/UserSession.ts @@ -755,6 +755,7 @@ export class UserSession implements IAuthenticationManager { this.tokenDuration = options.tokenDuration || 20160; this.redirectUri = options.redirectUri; this.refreshTokenTTL = options.refreshTokenTTL || 1440; + this.server = options.server; this.federatedServers = {}; this.trustedDomains = []; @@ -1092,107 +1093,90 @@ export class UserSession implements IAuthenticationManager { return this._pendingTokenRequests[root]; } - this._pendingTokenRequests[root] = this.getPortal().then((portalInfo) => { - /** - * Specific domains can be configured as secure.esri.com or https://secure.esri.com this - * normalizes to https://secure.esri.com so we can use startsWith later. - */ - if ( - portalInfo.authorizedCrossOriginDomains && - portalInfo.authorizedCrossOriginDomains.length - ) { - this.trustedDomains = portalInfo.authorizedCrossOriginDomains - .filter((d: string) => !d.startsWith("http://")) - .map((d: string) => { - if (d.startsWith("https://")) { - return d; + this._pendingTokenRequests[root] = this.fetchAuthorizedDomains().then( + () => { + return request(`${root}/rest/info`, { + credentials: this.getDomainCredentials(url), + }) + .then((response) => { + if (response.owningSystemUrl) { + /** + * if this server is not owned by this portal + * bail out with an error since we know we wont + * be able to generate a token + */ + if (!isFederated(response.owningSystemUrl, this.portal)) { + throw new ArcGISAuthError( + `${url} is not federated with ${this.portal}.`, + "NOT_FEDERATED" + ); + } else { + /** + * if the server is federated, use the relevant token endpoint. + */ + return request( + `${response.owningSystemUrl}/sharing/rest/info`, + requestOptions + ); + } + } else if ( + response.authInfo && + this.federatedServers[root] !== undefined + ) { + /** + * if its a stand-alone instance of ArcGIS Server that doesn't advertise + * federation, but the root server url is recognized, use its built in token endpoint. + */ + return Promise.resolve({ + authInfo: response.authInfo, + }); } else { - return `https://${d}`; - } - }); - } - - return request(`${root}/rest/info`, { - credentials: this.getDomainCredentials(url), - }) - .then((response) => { - if (response.owningSystemUrl) { - /** - * if this server is not owned by this portal - * bail out with an error since we know we wont - * be able to generate a token - */ - if (!isFederated(response.owningSystemUrl, this.portal)) { throw new ArcGISAuthError( - `${url} is not federated with ${this.portal}.`, + `${url} is not federated with any portal and is not explicitly trusted.`, "NOT_FEDERATED" ); + } + }) + .then((response: any) => { + return response.authInfo.tokenServicesUrl; + }) + .then((tokenServicesUrl: string) => { + // an expired token cant be used to generate a new token + if (this.token && this.tokenExpires.getTime() > Date.now()) { + return generateToken(tokenServicesUrl, { + params: { + token: this.token, + serverUrl: url, + expiration: this.tokenDuration, + client: "referer", + }, + }); + // generate an entirely fresh token if necessary } else { - /** - * if the server is federated, use the relevant token endpoint. - */ - return request( - `${response.owningSystemUrl}/sharing/rest/info`, - requestOptions - ); + return generateToken(tokenServicesUrl, { + params: { + username: this.username, + password: this.password, + expiration: this.tokenDuration, + client: "referer", + }, + }).then((response: any) => { + this._token = response.token; + this._tokenExpires = new Date(response.expires); + return response; + }); } - } else if ( - response.authInfo && - this.federatedServers[root] !== undefined - ) { - /** - * if its a stand-alone instance of ArcGIS Server that doesn't advertise - * federation, but the root server url is recognized, use its built in token endpoint. - */ - return Promise.resolve({ - authInfo: response.authInfo, - }); - } else { - throw new ArcGISAuthError( - `${url} is not federated with any portal and is not explicitly trusted.`, - "NOT_FEDERATED" - ); - } - }) - .then((response: any) => { - return response.authInfo.tokenServicesUrl; - }) - .then((tokenServicesUrl: string) => { - // an expired token cant be used to generate a new token - if (this.token && this.tokenExpires.getTime() > Date.now()) { - return generateToken(tokenServicesUrl, { - params: { - token: this.token, - serverUrl: url, - expiration: this.tokenDuration, - client: "referer", - }, - }); - // generate an entirely fresh token if necessary - } else { - return generateToken(tokenServicesUrl, { - params: { - username: this.username, - password: this.password, - expiration: this.tokenDuration, - client: "referer", - }, - }).then((response: any) => { - this._token = response.token; - this._tokenExpires = new Date(response.expires); - return response; - }); - } - }) - .then((response) => { - this.federatedServers[root] = { - expires: new Date(response.expires), - token: response.token, - }; - delete this._pendingTokenRequests[root]; - return response.token; - }); - }); + }) + .then((response) => { + this.federatedServers[root] = { + expires: new Date(response.expires), + token: response.token, + }; + delete this._pendingTokenRequests[root]; + return response.token; + }); + } + ); return this._pendingTokenRequests[root]; } @@ -1305,4 +1289,40 @@ export class UserSession implements IAuthenticationManager { } ); } + + /** + * ensures that the authorizedCrossOriginDomains are obtained from the portal and cached + * so we can check them later. + * + * @returns this + */ + private fetchAuthorizedDomains() { + // if this token is for a specific server or we don't have a portal + // don't get the portal info because we cant get the authorizedCrossOriginDomains + if (this.server || !this.portal) { + return Promise.resolve(this); + } + + return this.getPortal().then((portalInfo) => { + /** + * Specific domains can be configured as secure.esri.com or https://secure.esri.com this + * normalizes to https://secure.esri.com so we can use startsWith later. + */ + if ( + portalInfo.authorizedCrossOriginDomains && + portalInfo.authorizedCrossOriginDomains.length + ) { + this.trustedDomains = portalInfo.authorizedCrossOriginDomains + .filter((d: string) => !d.startsWith("http://")) + .map((d: string) => { + if (d.startsWith("https://")) { + return d; + } else { + return `https://${d}`; + } + }); + } + return this; + }); + } } diff --git a/packages/arcgis-rest-auth/src/generate-token.ts b/packages/arcgis-rest-auth/src/generate-token.ts index 44f99bb59a..15fea0566b 100644 --- a/packages/arcgis-rest-auth/src/generate-token.ts +++ b/packages/arcgis-rest-auth/src/generate-token.ts @@ -5,7 +5,7 @@ import { request, IRequestOptions, ITokenRequestOptions, - NODEJS_DEFAULT_REFERER_HEADER + NODEJS_DEFAULT_REFERER_HEADER, } from "@esri/arcgis-rest-request"; export interface IGenerateTokenResponse { diff --git a/packages/arcgis-rest-auth/test/UserSession.test.ts b/packages/arcgis-rest-auth/test/UserSession.test.ts index 13f54168f6..afeef3fd2e 100644 --- a/packages/arcgis-rest-auth/test/UserSession.test.ts +++ b/packages/arcgis-rest-auth/test/UserSession.test.ts @@ -199,6 +199,13 @@ describe("UserSession", () => { }, }); + fetchMock.getOnce( + "https://pnp00035.esri.com/portal/sharing/rest/portals/self?f=json&token=existing-session-token", + { + authorizedCrossOriginDomains: [], + } + ); + fetchMock.postOnce("https://pnp00035.esri.com/portal/sharing/rest/info", { owningSystemUrl: "https://pnp00035.esri.com/portal", authInfo: { @@ -306,6 +313,13 @@ describe("UserSession", () => { }, }); + fetchMock.getOnce( + "https://gis.city.gov/sharing/rest/portals/self?f=json&token=token", + { + authorizedCrossOriginDomains: [], + } + ); + fetchMock.postOnce("https://gis.city.gov/sharing/rest/info", { owningSystemUrl: "http://gis.city.gov", authInfo: { @@ -357,6 +371,13 @@ describe("UserSession", () => { }, }); + fetchMock.getOnce( + "https://gis.city.gov/sharing/rest/portals/self?f=json&token=token", + { + authorizedCrossOriginDomains: [], + } + ); + fetchMock.postOnce("https://gis.city.gov/sharing/rest/info", { owningSystemUrl: "http://gis.city.gov", authInfo: { @@ -406,6 +427,17 @@ describe("UserSession", () => { }, }); + fetchMock.postOnce("https://gis.city.gov/sharing/rest/generateToken", { + token: "portalToken", + }); + + fetchMock.getOnce( + "https://gis.city.gov/sharing/rest/portals/self?f=json&token=portalToken", + { + authorizedCrossOriginDomains: [], + } + ); + fetchMock.postOnce("https://gis.city.gov/sharing/rest/info", { owningSystemUrl: "http://gis.city.gov", authInfo: { @@ -455,6 +487,13 @@ describe("UserSession", () => { { repeat: 1, method: "POST" } ); + fetchMock.getOnce( + "https://gis.city.gov/sharing/rest/portals/self?f=json&token=token", + { + authorizedCrossOriginDomains: [], + } + ); + fetchMock.mock( "https://gis.city.gov/sharing/rest/info", { @@ -505,6 +544,20 @@ describe("UserSession", () => { tokenExpires: YESTERDAY, }); + // similates refreshing the token with the refresh token + fetchMock.postOnce("https://www.arcgis.com/sharing/rest/oauth2/token", { + access_token: "newToken", + expires_in: 60, + username: " c@sey", + }); + + fetchMock.getOnce( + "https://www.arcgis.com/sharing/rest/portals/self?f=json&token=newToken", + { + authorizedCrossOriginDomains: [], + } + ); + fetchMock.post("https://gisservices.city.gov/public/rest/info", { currentVersion: 10.51, fullVersion: "10.5.1.120", @@ -537,6 +590,19 @@ describe("UserSession", () => { tokenExpires: YESTERDAY, }); + fetchMock.postOnce("https://www.arcgis.com/sharing/rest/oauth2/token", { + access_token: "newToken", + expires_in: 60, + username: " c@sey", + }); + + fetchMock.getOnce( + "https://www.arcgis.com/sharing/rest/portals/self?f=json&token=newToken", + { + authorizedCrossOriginDomains: [], + } + ); + fetchMock.post("https://gisservices.city.gov/public/rest/info", { currentVersion: 10.51, fullVersion: "10.5.1.120", @@ -588,6 +654,19 @@ describe("UserSession", () => { fullVersion: "10.5.1.120", }); + fetchMock.postOnce("https://www.arcgis.com/sharing/rest/oauth2/token", { + access_token: "newToken", + expires_in: 60, + username: " c@sey", + }); + + fetchMock.getOnce( + "https://www.arcgis.com/sharing/rest/portals/self?f=json&token=newToken", + { + authorizedCrossOriginDomains: [], + } + ); + fetchMock.post( "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query", { @@ -1654,7 +1733,9 @@ describe("UserSession", () => { expect(session.portal).toEqual("https://www.arcgis.com/sharing/rest"); expect(session.ssl).toBeTruthy(); expect(session.token).toEqual("token"); - expect(session.tokenExpires).toEqual(new Date(Date.now() + 7200000 /* 2 hours */)); + expect(session.tokenExpires).toEqual( + new Date(Date.now() + 7200000 /* 2 hours */) + ); jasmine.clock().uninstall(); }); @@ -1839,4 +1920,327 @@ describe("UserSession", () => { }); }); }); + + describe(".getPortal()", () => { + afterEach(fetchMock.restore); + + it("should cache metadata about the portal", (done) => { + // we intentionally only mock one response + fetchMock.once( + "https://www.arcgis.com/sharing/rest/portals/self?f=json&token=token", + { + fetchAuthorizedDomains: ["gis.city.com"], + } + ); + + const session = new UserSession({ + clientId: "clientId", + redirectUri: "https://example-app.com/redirect-uri", + token: "token", + tokenExpires: TOMORROW, + refreshToken: "refreshToken", + refreshTokenExpires: TOMORROW, + refreshTokenTTL: 1440, + username: "jsmith", + password: "123456", + }); + + session + .getPortal() + .then((response) => { + expect(response.fetchAuthorizedDomains).toEqual(["gis.city.com"]); + session + .getPortal() + .then((cachedResponse) => { + expect(cachedResponse.fetchAuthorizedDomains).toEqual([ + "gis.city.com", + ]); + done(); + }) + .catch((e) => { + fail(e); + }); + }) + .catch((e) => { + fail(e); + }); + }); + + it("should never make more then 1 request", (done) => { + // we intentionally only mock one response + fetchMock.once( + "https://www.arcgis.com/sharing/rest/portals/self?f=json&token=token", + { + fetchAuthorizedDomains: ["gis.city.com"], + } + ); + + const session = new UserSession({ + clientId: "clientId", + redirectUri: "https://example-app.com/redirect-uri", + token: "token", + tokenExpires: TOMORROW, + refreshToken: "refreshToken", + refreshTokenExpires: TOMORROW, + refreshTokenTTL: 1440, + username: "jsmith", + password: "123456", + }); + + Promise.all([session.getPortal(), session.getPortal()]) + .then(() => { + done(); + }) + .catch((e) => { + fail(e); + }); + }); + }); + + describe("fetchAuthorizedDomains/getDomainCredentials", () => { + it("should default to same-origin credentials when no domains are listed in authorizedCrossOriginDomains", (done) => { + const session = new UserSession({ + clientId: "id", + token: "token", + refreshToken: "refresh", + tokenExpires: TOMORROW, + portal: "https://gis.city.gov/sharing/rest", + }); + + fetchMock.postOnce("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", + }, + }); + + fetchMock.getOnce( + "https://gis.city.gov/sharing/rest/portals/self?f=json&token=token", + { + authorizedCrossOriginDomains: [], + } + ); + + fetchMock.postOnce("https://gis.city.gov/sharing/rest/info", { + owningSystemUrl: "http://gis.city.gov", + authInfo: { + tokenServicesUrl: "https://gis.city.gov/sharing/generateToken", + isTokenBasedSecurity: true, + }, + }); + + fetchMock.postOnce("https://gis.city.gov/sharing/generateToken", { + token: "serverToken", + expires: TOMORROW, + }); + + fetchMock.post( + "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query", + { + count: 123, + } + ); + + request( + "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query", + { + authentication: session, + } + ) + .then((response) => { + const { credentials } = fetchMock.lastOptions( + "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query" + ); + expect(credentials).toEqual("same-origin"); + + done(); + }) + .catch((e) => { + fail(e); + }); + }); + + it("should set the credentials option to include when a server is listed in authorizedCrossOriginDomains", (done) => { + const session = new UserSession({ + clientId: "id", + token: "token", + refreshToken: "refresh", + tokenExpires: TOMORROW, + portal: "https://gis.city.gov/sharing/rest", + }); + + fetchMock.postOnce("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", + }, + }); + + fetchMock.getOnce( + "https://gis.city.gov/sharing/rest/portals/self?f=json&token=token", + { + authorizedCrossOriginDomains: ["https://gisservices.city.gov"], + } + ); + + fetchMock.postOnce("https://gis.city.gov/sharing/rest/info", { + owningSystemUrl: "http://gis.city.gov", + authInfo: { + tokenServicesUrl: "https://gis.city.gov/sharing/generateToken", + isTokenBasedSecurity: true, + }, + }); + + fetchMock.postOnce("https://gis.city.gov/sharing/generateToken", { + token: "serverToken", + expires: TOMORROW, + }); + + fetchMock.post( + "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query", + { + count: 123, + } + ); + + request( + "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query", + { + authentication: session, + } + ) + .then((response) => { + const { credentials } = fetchMock.lastOptions( + "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query" + ); + expect(credentials).toEqual("include"); + + done(); + }) + .catch((e) => { + fail(e); + }); + }); + }); + + it("should still send same-origin credentials even if another domain is listed in authorizedCrossOriginDomains", (done) => { + const session = new UserSession({ + clientId: "id", + token: "token", + refreshToken: "refresh", + tokenExpires: TOMORROW, + portal: "https://gis.city.gov/sharing/rest", + }); + + fetchMock.postOnce("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", + }, + }); + + fetchMock.getOnce( + "https://gis.city.gov/sharing/rest/portals/self?f=json&token=token", + { + authorizedCrossOriginDomains: ["https://other.city.gov"], + } + ); + + fetchMock.postOnce("https://gis.city.gov/sharing/rest/info", { + owningSystemUrl: "http://gis.city.gov", + authInfo: { + tokenServicesUrl: "https://gis.city.gov/sharing/generateToken", + isTokenBasedSecurity: true, + }, + }); + + fetchMock.postOnce("https://gis.city.gov/sharing/generateToken", { + token: "serverToken", + expires: TOMORROW, + }); + + fetchMock.post( + "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query", + { + count: 123, + } + ); + + request( + "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query", + { + authentication: session, + } + ) + .then((response) => { + const { credentials } = fetchMock.lastOptions( + "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query" + ); + expect(credentials).toEqual("same-origin"); + + done(); + }) + .catch((e) => { + fail(e); + }); + }); + + it("should normalize optional protocols in authorizedCrossOriginDomains", (done) => { + const session = new UserSession({ + clientId: "id", + token: "token", + refreshToken: "refresh", + tokenExpires: TOMORROW, + portal: "https://gis.city.gov/sharing/rest", + }); + + fetchMock.getOnce( + "https://gis.city.gov/sharing/rest/portals/self?f=json&token=token", + { + authorizedCrossOriginDomains: ["one.city.gov", "https://two.city.gov"], + } + ); + + (session as any) + .fetchAuthorizedDomains() + .then(() => { + expect((session as any).trustedDomains).toEqual([ + "https://one.city.gov", + "https://two.city.gov", + ]); + done(); + }) + .catch((e: Error) => { + fail(e); + }); + }); + + it("should not use domain credentials if portal is null", (done) => { + const session = new UserSession({ + clientId: "id", + token: "token", + refreshToken: "refresh", + tokenExpires: TOMORROW, + portal: null, + server: "https://fakeserver.com/arcgis", + }); + + (session as any) + .fetchAuthorizedDomains() + .then(() => { + done(); + }) + .catch((e: Error) => { + fail(e); + }); + }); }); diff --git a/packages/arcgis-rest-request/src/request.ts b/packages/arcgis-rest-request/src/request.ts index 32a977513e..7f1dc7393d 100644 --- a/packages/arcgis-rest-request/src/request.ts +++ b/packages/arcgis-rest-request/src/request.ts @@ -408,6 +408,7 @@ export function request( options, originalAuthError ); + if (originalAuthError) { /* if the request was made to an unfederated service that didnt require authentication, add the base url and a dummy token @@ -416,7 +417,8 @@ export function request( const truncatedUrl: string = url .toLowerCase() .split(/\/rest(\/admin)?\/services\//)[0]; - (options.authentication as any).trustedServers[truncatedUrl] = { + + (options.authentication as any).federatedServers[truncatedUrl] = { token: [], // default to 24 hours expires: new Date(Date.now() + 86400 * 1000), diff --git a/packages/arcgis-rest-service-admin/test/addTo.test.ts b/packages/arcgis-rest-service-admin/test/addTo.test.ts index dcfc694a72..695ab47d40 100644 --- a/packages/arcgis-rest-service-admin/test/addTo.test.ts +++ b/packages/arcgis-rest-service-admin/test/addTo.test.ts @@ -64,22 +64,22 @@ describe("add to feature service", () => { const tableDescriptionFayard: ITable = { name: "Fayard", - id: "1914", + id: 1914, }; const tableDescriptionHarold: ITable = { name: "Harold", - id: "1921", + id: 1921, }; const tableDescriptionGene: ITable = { name: "Gene", - id: "1912", + id: 1912, }; const tableDescriptionFail: ITable = { name: "", - id: "0", + id: 0, }; it("should add a pair of layers", (done) => { From 7a29ea6547011e66d661d45947016aa5a5fecab3 Mon Sep 17 00:00:00 2001 From: Patrick Arlt Date: Thu, 6 May 2021 11:21:12 -0700 Subject: [PATCH 4/6] Update packages/arcgis-rest-auth/src/UserSession.ts Co-authored-by: Noah Mulfinger --- packages/arcgis-rest-auth/src/UserSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/arcgis-rest-auth/src/UserSession.ts b/packages/arcgis-rest-auth/src/UserSession.ts index 2c7f9a5733..007aa63b48 100644 --- a/packages/arcgis-rest-auth/src/UserSession.ts +++ b/packages/arcgis-rest-auth/src/UserSession.ts @@ -831,7 +831,7 @@ export class UserSession implements IAuthenticationManager { } /** - * Returns information about the currently logged in users [portal](https://developers.arcgis.com/rest/users-groups-and-items/portal-self.htm). Subsequent calls will *not* result in additional web traffic. + * Returns information about the currently logged in user's [portal](https://developers.arcgis.com/rest/users-groups-and-items/portal-self.htm). Subsequent calls will *not* result in additional web traffic. * * ```js * session.getPortal() From e212be3ba6b5f1afd56860d12e997df17098ea77 Mon Sep 17 00:00:00 2001 From: Patrick Arlt Date: Thu, 6 May 2021 13:09:37 -0700 Subject: [PATCH 5/6] final feedback --- packages/arcgis-rest-auth/src/UserSession.ts | 19 +++++++++++++++---- .../arcgis-rest-auth/test/UserSession.test.ts | 4 ++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/arcgis-rest-auth/src/UserSession.ts b/packages/arcgis-rest-auth/src/UserSession.ts index 2c7f9a5733..be52b43b4f 100644 --- a/packages/arcgis-rest-auth/src/UserSession.ts +++ b/packages/arcgis-rest-auth/src/UserSession.ts @@ -277,6 +277,17 @@ export class UserSession implements IAuthenticationManager { get refreshTokenExpires() { return this._refreshTokenExpires; } + + /** + * Deprecated, use `federatedServers` instead. + * @deprecated + + */ + get trustedServers() { + console.log("DEPRECATED: use federatedServers instead"); + return this.federatedServers; + } + /** * Begins a new browser-based OAuth 2.0 sign in. If `options.popup` is `true` the * authentication window will open in a new tab/window otherwise the user will @@ -703,7 +714,7 @@ export class UserSession implements IAuthenticationManager { /** * Hydrated by a call to [getPortal()](#getPortal-summary). */ - private _portal: any; + private _portalInfo: any; private _token: string; private _tokenExpires: Date; @@ -846,8 +857,8 @@ export class UserSession implements IAuthenticationManager { public getPortal(requestOptions?: IRequestOptions): Promise { if (this._pendingPortalRequest) { return this._pendingPortalRequest; - } else if (this._portal) { - return Promise.resolve(this._portal); + } else if (this._portalInfo) { + return Promise.resolve(this._portalInfo); } else { const url = `${this.portal}/portals/self`; @@ -859,7 +870,7 @@ export class UserSession implements IAuthenticationManager { } as IRequestOptions; this._pendingPortalRequest = request(url, options).then((response) => { - this._portal = response; + this._portalInfo = response; this._pendingPortalRequest = null; return response; }); diff --git a/packages/arcgis-rest-auth/test/UserSession.test.ts b/packages/arcgis-rest-auth/test/UserSession.test.ts index afeef3fd2e..be66e1ad57 100644 --- a/packages/arcgis-rest-auth/test/UserSession.test.ts +++ b/packages/arcgis-rest-auth/test/UserSession.test.ts @@ -1929,7 +1929,7 @@ describe("UserSession", () => { fetchMock.once( "https://www.arcgis.com/sharing/rest/portals/self?f=json&token=token", { - fetchAuthorizedDomains: ["gis.city.com"], + authorizedCrossOriginDomains: ["gis.city.com"], } ); @@ -1971,7 +1971,7 @@ describe("UserSession", () => { fetchMock.once( "https://www.arcgis.com/sharing/rest/portals/self?f=json&token=token", { - fetchAuthorizedDomains: ["gis.city.com"], + authorizedCrossOriginDomains: ["gis.city.com"], } ); From 132f13355bbecfb340612e5cb62e63fcf359a9ce Mon Sep 17 00:00:00 2001 From: Patrick Arlt Date: Thu, 6 May 2021 14:51:54 -0700 Subject: [PATCH 6/6] review feedback --- packages/arcgis-rest-auth/src/UserSession.ts | 2 +- .../arcgis-rest-auth/test/UserSession.test.ts | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/arcgis-rest-auth/src/UserSession.ts b/packages/arcgis-rest-auth/src/UserSession.ts index 40b94ca481..7e45ccafdb 100644 --- a/packages/arcgis-rest-auth/src/UserSession.ts +++ b/packages/arcgis-rest-auth/src/UserSession.ts @@ -280,8 +280,8 @@ export class UserSession implements IAuthenticationManager { /** * Deprecated, use `federatedServers` instead. + * * @deprecated - */ get trustedServers() { console.log("DEPRECATED: use federatedServers instead"); diff --git a/packages/arcgis-rest-auth/test/UserSession.test.ts b/packages/arcgis-rest-auth/test/UserSession.test.ts index be66e1ad57..9348708caf 100644 --- a/packages/arcgis-rest-auth/test/UserSession.test.ts +++ b/packages/arcgis-rest-auth/test/UserSession.test.ts @@ -1948,11 +1948,13 @@ describe("UserSession", () => { session .getPortal() .then((response) => { - expect(response.fetchAuthorizedDomains).toEqual(["gis.city.com"]); + expect(response.authorizedCrossOriginDomains).toEqual([ + "gis.city.com", + ]); session .getPortal() .then((cachedResponse) => { - expect(cachedResponse.fetchAuthorizedDomains).toEqual([ + expect(cachedResponse.authorizedCrossOriginDomains).toEqual([ "gis.city.com", ]); done(); @@ -2243,4 +2245,15 @@ describe("UserSession", () => { fail(e); }); }); + + it("should deprecate trustedServers", () => { + const session = new UserSession({ + clientId: "id", + token: "token", + }); + + expect((session as any).trustedServers).toBe( + (session as any).federatedServers + ); + }); });