diff --git a/.travis.yml b/.travis.yml index 6868d9f9d1..e003c34624 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,10 @@ notifications: slack: secure: KghYpsaoCZB1sPOqk1d+JhfBOkg76Cq8eMK665ozdf484RdPvObqLrxyrt9i+sDMDv/oZT+VuvGXsbHj6R+TUh87r2Yhfizcwvdotc4VTHei4y/oNX6cRhV8ryQox/0j6GClqI43A4kIsuOcUm8pI4FTGOhDQOK7+UDcuvcSa5VR7N7GJ97zfzC9gQtRsbfD5WKY4BEr+KlcWt0KCCbp1+Jh7craEO30ztUl38BuUuW7F1/AykrTX6kgS5E5ssUQ0pJPBlzpjb3G5UEeodzOD9wUBVLIBAbgQsofAPLi0pHeyCCeEOuTxXEo7SD8fG+kBstPlncR+V8goCEBnAJysIvBznUxLjrnxcU7V40g+iTENKwhVTIiZzZN3Z9buJd001MRaWjOTCWX80Ig6oraH8WJk7W1qtKzsDK9Lsp2N9pxNTPASbAQoFDlMCGcWYGixq6IhIAwsk1Pkb689F67vHaES8qMXkIHifB0j7yRMwrHinYFqUYASQOECPNEMJv3iqByXntmMP8X/r3VBujMehGodZ1dnbWvAOdWdEmG3KCVpn42ocKCj6k4WZL62nSxs3W00E1UVPnXujT3ENk/3L/VJE3tRWFCSbR9HnfBSDVA6WaFu4EFDg8WV6fn2T0FHKTBgCtTugQo8bsoyReYZvEU/27O/B+d5/9nqMHqyy4= node_js: - - "node" + # Currently `npm ci` fails when installing a GH repo dep that has a prepare script that uses a command from a devDependency + # https://github.com/npm/cli/issues/2084 is the issue, it's closed but node has not cut a release yet 11/9/2020 + # Will return this once it's resolved + # - "node" - "lts/*" - 10 cache: npm diff --git a/packages/arcgis-rest-auth/src/UserSession.ts b/packages/arcgis-rest-auth/src/UserSession.ts index aa88b4b44e..9e834172df 100644 --- a/packages/arcgis-rest-auth/src/UserSession.ts +++ b/packages/arcgis-rest-auth/src/UserSession.ts @@ -21,12 +21,13 @@ import { ITokenRequestOptions, cleanUrl, encodeQueryString, - decodeQueryString + decodeQueryString, } from "@esri/arcgis-rest-request"; import { IUser } from "@esri/arcgis-rest-types"; import { generateToken } from "./generate-token"; import { fetchToken, IFetchTokenResponse } from "./fetch-token"; import { canUseOnlineToken, isFederated } from "./federation-utils"; +import { IAppAccess, validateAppAccess } from "./validate-app-access"; /** * Internal utility for resolving a Promise from outside its constructor. @@ -62,7 +63,7 @@ function defer(): IDeferred { const deferred: any = { promise: null, resolve: null, - reject: null + reject: null, }; deferred.promise = new Promise((resolve, reject) => { @@ -257,7 +258,6 @@ export interface IUserSessionOptions { * Used to authenticate both ArcGIS Online and ArcGIS Enterprise users. `UserSession` includes helper methods for [OAuth 2.0](/arcgis-rest-js/guides/browser-authentication/) in both browser and server applications. */ export class UserSession implements IAuthenticationManager { - /** * The current ArcGIS Online or ArcGIS Enterprise `token`. */ @@ -303,7 +303,7 @@ export class UserSession implements IAuthenticationManager { popup, state, locale, - params + params, }: IOAuth2Options = { ...{ portal: "https://www.arcgis.com/sharing/rest", @@ -311,9 +311,9 @@ export class UserSession implements IAuthenticationManager { duration: 20160, popup: true, state: options.clientId, - locale: "" + locale: "", }, - ...options + ...options, }; let url: string; if (provider === "arcgis") { @@ -357,7 +357,7 @@ export class UserSession implements IAuthenticationManager { ssl: oauthInfo.ssl, token: oauthInfo.token, tokenExpires: new Date(oauthInfo.expires), - username: oauthInfo.username + username: oauthInfo.username, }) ); } @@ -384,7 +384,7 @@ export class UserSession implements IAuthenticationManager { public static completeOAuth2(options: IOAuth2Options, win: any = window) { const { portal, clientId, popup }: IOAuth2Options = { ...{ portal: "https://www.arcgis.com/sharing/rest", popup: true }, - ...options + ...options, }; function completeSignIn(error: any, oauthInfo?: IFetchTokenResponse) { @@ -433,7 +433,7 @@ export class UserSession implements IAuthenticationManager { ssl: oauthInfo.ssl, token: oauthInfo.token, tokenExpires: oauthInfo.expires, - username: oauthInfo.username + username: oauthInfo.username, }); } @@ -462,24 +462,20 @@ export class UserSession implements IAuthenticationManager { token, expires, ssl, - username + username, }); } /** * Request session information from the parent application - * + * * When an application is embedded into another application via an IFrame, the embedded app can - * use `window.postMessage` to request credentials from the host application. - * + * use `window.postMessage` to request credentials from the host application. + * * @param parentOrigin origin of the parent frame. Passed into the embedded application as `parentOrigin` query param * @browserOnly */ - public static fromParent ( - parentOrigin:string, - win?: any - ): Promise { - + public static fromParent(parentOrigin: string, win?: any): Promise { /* istanbul ignore next: must pass in a mockwindow for tests so we can't cover the other branch */ if (!win && window) { win = window; @@ -490,10 +486,10 @@ export class UserSession implements IAuthenticationManager { // session information from the correct origin return new Promise((resolve, reject) => { // create an event handler that just wraps the parentMessageHandler - handler = (event:any) => { + handler = (event: any) => { // ensure we only listen to events from the specified parent // if the origin is not the parent origin, we don't send any response - if (event.origin === parentOrigin){ + if (event.origin === parentOrigin) { try { return resolve(UserSession.parentMessageHandler(event)); } catch (err) { @@ -502,11 +498,13 @@ export class UserSession implements IAuthenticationManager { } }; // add listener - win.addEventListener('message', handler, false); - win.parent.postMessage({type: 'arcgis:auth:requestCredential'}, parentOrigin); - }) - .then((session) => { - win.removeEventListener('message', handler, false); + win.addEventListener("message", handler, false); + win.parent.postMessage( + { type: "arcgis:auth:requestCredential" }, + parentOrigin + ); + }).then((session) => { + win.removeEventListener("message", handler, false); return session; }); } @@ -523,13 +521,13 @@ export class UserSession implements IAuthenticationManager { ) { const { portal, clientId, duration, redirectUri }: IOAuth2Options = { ...{ portal: "https://arcgis.com/sharing/rest", duration: 20160 }, - ...options + ...options, }; response.writeHead(301, { Location: `${portal}/oauth2/authorize?client_id=${clientId}&duration=${duration}&response_type=code&redirect_uri=${encodeURIComponent( redirectUri - )}` + )}`, }); response.end(); @@ -548,9 +546,9 @@ export class UserSession implements IAuthenticationManager { const { portal, clientId, redirectUri, refreshTokenTTL }: IOAuth2Options = { ...{ portal: "https://www.arcgis.com/sharing/rest", - refreshTokenTTL: 1440 + refreshTokenTTL: 1440, }, - ...options + ...options, }; return fetchToken(`${portal}/oauth2/token`, { @@ -558,9 +556,9 @@ export class UserSession implements IAuthenticationManager { grant_type: "authorization_code", client_id: clientId, redirect_uri: redirectUri, - code: authorizationCode - } - }).then(response => { + code: authorizationCode, + }, + }).then((response) => { return new UserSession({ clientId, portal, @@ -573,7 +571,7 @@ export class UserSession implements IAuthenticationManager { ), token: response.token, tokenExpires: response.expires, - username: response.username + username: response.username, }); }); } @@ -592,7 +590,7 @@ export class UserSession implements IAuthenticationManager { ssl: options.ssl, tokenDuration: options.tokenDuration, redirectUri: options.redirectUri, - refreshTokenTTL: options.refreshTokenTTL + refreshTokenTTL: options.refreshTokenTTL, }); } @@ -616,7 +614,7 @@ export class UserSession implements IAuthenticationManager { ssl: credential.ssl, token: credential.token, username: credential.userId, - tokenExpires: new Date(credential.expires) + tokenExpires: new Date(credential.expires), }); } @@ -624,14 +622,14 @@ export class UserSession implements IAuthenticationManager { * Handle the response from the parent * @param event DOM Event */ - private static parentMessageHandler (event:any):UserSession { - if (event.data.type === 'arcgis:auth:credential') { + private static parentMessageHandler(event: any): UserSession { + if (event.data.type === "arcgis:auth:credential") { return UserSession.fromCredential(event.data.credential); } - if (event.data.type === 'arcgis:auth:rejected') { + if (event.data.type === "arcgis:auth:rejected") { throw new Error(event.data.message); } else { - throw new Error('Unknown message type.'); + throw new Error("Unknown message type."); } } @@ -749,7 +747,7 @@ export class UserSession implements IAuthenticationManager { this.trustedServers[root] = { token: options.token, - expires: options.tokenExpires + expires: options.tokenExpires, }; } this._pendingTokenRequests = {}; @@ -770,7 +768,7 @@ export class UserSession implements IAuthenticationManager { server: this.portal, ssl: this.ssl, token: this.token, - userId: this.username + userId: this.username, }; } @@ -799,10 +797,10 @@ export class UserSession implements IAuthenticationManager { httpMethod: "GET", authentication: this, ...requestOptions, - rawResponse: false + rawResponse: false, } as IRequestOptions; - this._pendingUserRequest = request(url, options).then(response => { + this._pendingUserRequest = request(url, options).then((response) => { this._user = response; this._pendingUserRequest = null; return response; @@ -828,7 +826,7 @@ export class UserSession implements IAuthenticationManager { } else if (this._user) { return Promise.resolve(this._user.username); } else { - return this.getUser().then(user => { + return this.getUser().then((user) => { return user.username; }); } @@ -851,6 +849,18 @@ export class UserSession implements IAuthenticationManager { } } + /** + * Get application access information for the current user + * see `validateAppAccess` function for details + * + * @param clientId application client id + */ + public validateAppAccess(clientId: string): Promise { + return this.getToken(this.portal).then((token) => { + return validateAppAccess(token, clientId); + }); + } + public toJSON(): IUserSessionOptions { return { clientId: this.clientId, @@ -864,7 +874,7 @@ export class UserSession implements IAuthenticationManager { ssl: this.ssl, tokenDuration: this.tokenDuration, redirectUri: this.redirectUri, - refreshTokenTTL: this.refreshTokenTTL + refreshTokenTTL: this.refreshTokenTTL, }; } @@ -874,19 +884,19 @@ export class UserSession implements IAuthenticationManager { /** * For a "Host" app that embeds other platform apps via iframes, after authenticating the user * and creating a UserSession, the app can then enable "post message" style authentication by calling - * this method. - * + * this method. + * * Internally this adds an event listener on window for the `message` event - * + * * @param validChildOrigins Array of origins that are allowed to request authentication from the host app */ - public enablePostMessageAuth (validChildOrigins: string[], win?:any ): any { + public enablePostMessageAuth(validChildOrigins: string[], win?: any): any { /* istanbul ignore next: must pass in a mockwindow for tests so we can't cover the other branch */ if (!win && window) { win = window; } this._hostHandler = this.createPostMessageHandler(validChildOrigins); - win.addEventListener('message',this._hostHandler , false); + win.addEventListener("message", this._hostHandler, false); } /** @@ -894,12 +904,12 @@ export class UserSession implements IAuthenticationManager { * to transition routes, it should call `UserSession.disablePostMessageAuth()` to remove * the event listener and prevent memory leaks */ - public disablePostMessageAuth (win?: any) { + public disablePostMessageAuth(win?: any) { /* istanbul ignore next: must pass in a mockwindow for tests so we can't cover the other branch */ if (!win && window) { win = window; } - win.removeEventListener('message', this._hostHandler, false); + win.removeEventListener("message", this._hostHandler, false); } /** @@ -939,27 +949,36 @@ export class UserSession implements IAuthenticationManager { return `${protocol}${domain.toLowerCase()}/${path.join("/")}`; } /** - * Return a function that closes over the validOrigins array and + * Return a function that closes over the validOrigins array and * can be used as an event handler for the `message` event - * + * * @param validOrigins Array of valid origins */ - private createPostMessageHandler ( + private createPostMessageHandler( validOrigins: string[] - ): (event:any) => void { + ): (event: any) => void { // return a function that closes over the validOrigins and // has access to the credential - return (event:any) => { + return (event: any) => { // Note: do not use regex's here. validOrigins is an array so we're checking that the event's origin // is in the array via exact match. More info about avoiding postMessave xss issues here // https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html#tipsbypasses-in-postmessage-vulnerabilities if (validOrigins.indexOf(event.origin) > -1) { const credential = this.toCredential(); - event.source.postMessage({type: 'arcgis:auth:credential', credential}, event.origin); + event.source.postMessage( + { type: "arcgis:auth:credential", credential }, + event.origin + ); } else { - event.source.postMessage({type: 'arcgis:auth:rejected', message: `Rejected authentication request.`}, event.origin); + event.source.postMessage( + { + type: "arcgis:auth:rejected", + message: `Rejected authentication request.`, + }, + event.origin + ); } - } + }; } /** @@ -988,7 +1007,7 @@ export class UserSession implements IAuthenticationManager { } this._pendingTokenRequests[root] = request(`${root}/rest/info`) - .then(response => { + .then((response) => { if (response.owningSystemUrl) { /** * if this server is not owned by this portal @@ -1036,8 +1055,8 @@ export class UserSession implements IAuthenticationManager { token: this.token, serverUrl: url, expiration: this.tokenDuration, - client: "referer" - } + client: "referer", + }, }); // generate an entirely fresh token if necessary } else { @@ -1046,8 +1065,8 @@ export class UserSession implements IAuthenticationManager { username: this.username, password: this.password, expiration: this.tokenDuration, - client: "referer" - } + client: "referer", + }, }).then((response: any) => { this._token = response.token; this._tokenExpires = new Date(response.expires); @@ -1055,10 +1074,10 @@ export class UserSession implements IAuthenticationManager { }); } }) - .then(response => { + .then((response) => { this.trustedServers[root] = { expires: new Date(response.expires), - token: response.token + token: response.token, }; delete this._pendingTokenRequests[root]; return response.token; @@ -1086,7 +1105,7 @@ export class UserSession implements IAuthenticationManager { if (!this._pendingTokenRequests[this.portal]) { this._pendingTokenRequests[this.portal] = this.refreshSession( requestOptions - ).then(session => { + ).then((session) => { this._pendingTokenRequests[this.portal] = null; return session.token; }); @@ -1106,9 +1125,9 @@ export class UserSession implements IAuthenticationManager { params: { username: this.username, password: this.password, - expiration: this.tokenDuration + expiration: this.tokenDuration, }, - ...requestOptions + ...requestOptions, }; return generateToken(`${this.portal}/generateToken`, options).then( (response: any) => { @@ -1135,15 +1154,17 @@ export class UserSession implements IAuthenticationManager { params: { client_id: this.clientId, refresh_token: this.refreshToken, - grant_type: "refresh_token" + grant_type: "refresh_token", }, - ...requestOptions + ...requestOptions, }; - return fetchToken(`${this.portal}/oauth2/token`, options).then(response => { - this._token = response.token; - this._tokenExpires = response.expires; - return this; - }); + return fetchToken(`${this.portal}/oauth2/token`, options).then( + (response) => { + this._token = response.token; + this._tokenExpires = response.expires; + return this; + } + ); } /** @@ -1156,19 +1177,21 @@ export class UserSession implements IAuthenticationManager { client_id: this.clientId, refresh_token: this.refreshToken, redirect_uri: this.redirectUri, - grant_type: "exchange_refresh_token" + grant_type: "exchange_refresh_token", }, - ...requestOptions + ...requestOptions, }; - return fetchToken(`${this.portal}/oauth2/token`, options).then(response => { - this._token = response.token; - this._tokenExpires = response.expires; - this._refreshToken = response.refreshToken; - this._refreshTokenExpires = new Date( - Date.now() + (this.refreshTokenTTL - 1) * 60 * 1000 - ); - return this; - }); + return fetchToken(`${this.portal}/oauth2/token`, options).then( + (response) => { + this._token = response.token; + this._tokenExpires = response.expires; + this._refreshToken = response.refreshToken; + this._refreshTokenExpires = new Date( + Date.now() + (this.refreshTokenTTL - 1) * 60 * 1000 + ); + return this; + } + ); } } diff --git a/packages/arcgis-rest-auth/src/app-tokens.ts b/packages/arcgis-rest-auth/src/app-tokens.ts new file mode 100644 index 0000000000..24e696334e --- /dev/null +++ b/packages/arcgis-rest-auth/src/app-tokens.ts @@ -0,0 +1,84 @@ +/* Copyright (c) 2018-2020 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { IRequestOptions, request } from "@esri/arcgis-rest-request"; + +/** + * Request app-specific token, passing in the token for the current app. + * + * This call returns a token after performing the same checks made by validateAppAccess. + * It returns an app-specific token of the signed-in user only if the user has access + * to the app and the encrypted platform cookie is valid. + * + * A scenario where an app would use this is if it is iframed into another platform app + * and recieves credentials via postMessage. Those credentials contain a token that is + * specific to the host app, so the embedded app would use `exchangeToken` to get one + * that is specific to itself. + * + * Note: This is only usable by Esri applications hosted on *arcgis.com, *esri.com or within + * an ArcGIS Enterprise installation. Custom applications can not use this. + * + * @param token + * @param clientId application + * @param portal + */ +export function exchangeToken( + token: string, + clientId: string, + portal: string = "https://www.arcgis.com/sharing/rest" +): Promise { + const url = `${portal}/oauth2/exchangeToken`; + const ro = { + method: "POST", + params: { + f: "json", + client_id: clientId, + token, + }, + } as IRequestOptions; + // make the request and return the token + return request(url, ro).then((response) => response.token); +} + +export interface IPlatformSelfResponse { + username: string; + token: string; +} + +/** + * Request a token for a specific application using the esri_aopc encrypted cookie + * + * When a client app boots up, it will know it's clientId and the redirectUri for use + * in the normal /oauth/authorize pop-out oAuth flow. + * + * If the app sees an `esri_aopc` cookie (only set if the app is hosted on *.arcgis.com), + * it can call the /oauth2/platformSelf end-point passing in the clientId and redirectUri + * in headers, and it will recieve back an app-specific token, assuming the user has + * access to the app. + * + * Since there are scenarios where an app can boot using credintials/token from localstorage + * but those creds are not for the same user as the esri_aopc cookie, it is recommended that + * an app check the returned username against any existing identity they may have loaded. + * + * Note: This is only usable by Esri applications hosted on *arcgis.com, *esri.com or within + * an ArcGIS Enterprise installation. Custom applications can not use this. + * @param clientId + * @param redirectUri + * @param portal + */ +export function platformSelf( + clientId: string, + redirectUri: string, + portal: string = "https://www.arcgis.com/sharing/rest" +): Promise { + const url = `${portal}/oauth2/platformSelf`; + const ro = { + method: "POST", + headers: { + "X-Esri-Auth-Client-Id": clientId, + "X-Esri-Auth-Redirect-Uri": redirectUri, + }, + } as IRequestOptions; + // make the request and return the token + return request(url, ro); +} diff --git a/packages/arcgis-rest-auth/src/validate-app-access.ts b/packages/arcgis-rest-auth/src/validate-app-access.ts new file mode 100644 index 0000000000..b6db6483a2 --- /dev/null +++ b/packages/arcgis-rest-auth/src/validate-app-access.ts @@ -0,0 +1,68 @@ +/* Copyright (c) 2018-2020 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { IRequestOptions, request } from "@esri/arcgis-rest-request"; + +export interface IAppAccess { + /** + * Verifies that the token is valid and the user has access to + * the specified app (clientId) + */ + valid: boolean; + /** + * Should the app present the current user with a "View Only" mode + */ + viewOnlyUserTypeApp: boolean; +} + +/** + * Validates that the user has access to the application + * and if they user should be presented a "View Only" mode + * + * This is only needed/valid for Esri applications that are "licensed" + * and shipped in ArcGIS Online or ArcGIS Enterprise. Most custom applications + * should not need or use this. + * + * ```js + * import { validateAppAccess } from '@esri/arcgis-rest-auth'; + * + * return validateAppAccess('your-token', 'theClientId') + * .then((result) => { + * if (!result.valud) { + * // redirect or show some other ui + * } else { + * if (result.viewOnlyUserTypeApp) { + * // use this to inform your app to show a "View Only" mode + * } + * } + * }) + * .catch((err) => { + * // two possible errors + * // invalid clientId: {"error":{"code":400,"messageCode":"GWM_0007","message":"Invalid request","details":[]}} + * // invalid token: {"error":{"code":498,"message":"Invalid token.","details":[]}} + * }) + * ``` + * + * Note: This is only usable by Esri applications hosted on *arcgis.com, *esri.com or within + * an ArcGIS Enterprise installation. Custom applications can not use this. + * + * @param token platform token + * @param clientId application client id + * @param portal Optional + */ +export function validateAppAccess( + token: string, + clientId: string, + portal: string = "https://www.arcgis.com/sharing/rest" +): Promise { + const url = `${portal}/oauth2/validateAppAccess`; + const ro = { + method: "POST", + params: { + f: "json", + client_id: clientId, + token, + }, + } as IRequestOptions; + return request(url, ro); +} diff --git a/packages/arcgis-rest-auth/test/UserSession.test.ts b/packages/arcgis-rest-auth/test/UserSession.test.ts index fe79d443d3..ad33a2f099 100644 --- a/packages/arcgis-rest-auth/test/UserSession.test.ts +++ b/packages/arcgis-rest-auth/test/UserSession.test.ts @@ -1,7 +1,6 @@ /* Copyright (c) 2018 Environmental Systems Research Institute, Inc. * Apache-2.0 */ - /* tslint:disable:no-empty */ import { UserSession } from "../src/index"; import { ICredential } from "../src/UserSession"; @@ -10,7 +9,7 @@ import { request, ArcGISRequestError, ArcGISAuthError, - ErrorTypes + ErrorTypes, } from "@esri/arcgis-rest-request"; import * as fetchMock from "fetch-mock"; import { YESTERDAY, TOMORROW } from "./utils"; @@ -28,7 +27,7 @@ describe("UserSession", () => { refreshTokenExpires: TOMORROW, refreshTokenTTL: 1440, username: "c@sey", - password: "123456" + password: "123456", }); const session2 = UserSession.deserialize(session.serialize()); @@ -49,132 +48,132 @@ describe("UserSession", () => { }); describe(".getToken()", () => { - it("should return unexpired tokens for trusted arcgis.com domains", done => { + it("should return unexpired tokens for trusted arcgis.com domains", (done) => { const session = new UserSession({ clientId: "id", token: "token", - tokenExpires: TOMORROW + tokenExpires: TOMORROW, }); Promise.all([ session.getToken("https://www.arcgis.com/sharing/rest/portals/self"), session.getToken( "https://services1.arcgis.com/MOCK_ORG/arcgis/rest/services/Private_Service/FeatureServer" - ) + ), ]) .then(([token1, token2]) => { expect(token1).toBe("token"); expect(token2).toBe("token"); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should return unexpired tokens when an org url is passed", done => { + it("should return unexpired tokens when an org url is passed", (done) => { const session = new UserSession({ clientId: "id", token: "token", tokenExpires: TOMORROW, - portal: "https://custom.maps.arcgis.com/sharing/rest" + portal: "https://custom.maps.arcgis.com/sharing/rest", }); session .getToken( "https://services1.arcgis.com/MOCK_ORG/arcgis/rest/services/Private_Service/FeatureServer" ) - .then(token => { + .then((token) => { expect(token).toBe("token"); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should return unexpired tokens when an org url is passed on other ArcGIS Online environments", done => { + it("should return unexpired tokens when an org url is passed on other ArcGIS Online environments", (done) => { const session = new UserSession({ clientId: "id", token: "token", tokenExpires: TOMORROW, - portal: "https://custom.mapsdev.arcgis.com/sharing/rest" + portal: "https://custom.mapsdev.arcgis.com/sharing/rest", }); session .getToken( "https://services1dev.arcgis.com/MOCK_ORG/arcgis/rest/services/Private_Service/FeatureServer" ) - .then(token => { + .then((token) => { expect(token).toBe("token"); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should return unexpired tokens when there is an http/https mismatch", done => { + it("should return unexpired tokens when there is an http/https mismatch", (done) => { const session = new UserSession({ clientId: "id", token: "token", tokenExpires: TOMORROW, - portal: "http://custom.mapsdev.arcgis.com/sharing/rest" + portal: "http://custom.mapsdev.arcgis.com/sharing/rest", }); session .getToken( "https://services1dev.arcgis.com/MOCK_ORG/arcgis/rest/services/Private_Service/FeatureServer" ) - .then(token => { + .then((token) => { expect(token).toBe("token"); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should return unexpired tokens for the configured portal domain", done => { + it("should return unexpired tokens for the configured portal domain", (done) => { const session = new UserSession({ clientId: "id", token: "token", tokenExpires: TOMORROW, - portal: "https://gis.city.gov/sharing/rest" + portal: "https://gis.city.gov/sharing/rest", }); session .getToken("https://gis.city.gov/sharing/rest/portals/self") - .then(token => { + .then((token) => { expect(token).toBe("token"); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should return unexpired tokens for the configured portal domain, regardless of CASING", done => { + it("should return unexpired tokens for the configured portal domain, regardless of CASING", (done) => { // This was a real configuration discovered on a portal instance const session = new UserSession({ clientId: "id", token: "token", tokenExpires: TOMORROW, - portal: "https://pnp00035.esri.com/sharing/rest" + portal: "https://pnp00035.esri.com/sharing/rest", }); session .getToken("https://PNP00035.esri.com/sharing/rest/portals/self") - .then(token => { + .then((token) => { expect(token).toBe("token"); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should fetch token when contacting a server that is federated, even if on same domain, regardless of domain casing", done => { + it("should fetch token when contacting a server that is federated, even if on same domain, regardless of domain casing", (done) => { // This was a real configuration discovered on a portal instance // apparently when federating servers, the UI does not force the // server url to lowercase, and this any feature service items generated @@ -186,7 +185,7 @@ describe("UserSession", () => { token: "existing-session-token", refreshToken: "refresh", tokenExpires: TOMORROW, - portal: "https://pnp00035.esri.com/portal/sharing/rest" + portal: "https://pnp00035.esri.com/portal/sharing/rest", }); fetchMock.postOnce("https://pnp00035.esri.com/server/rest/info", { @@ -196,8 +195,8 @@ describe("UserSession", () => { authInfo: { isTokenBasedSecurity: true, tokenServicesUrl: - "https://pnp00035.esri.com/portal/sharing/rest/generateToken" - } + "https://pnp00035.esri.com/portal/sharing/rest/generateToken", + }, }); fetchMock.postOnce("https://pnp00035.esri.com/portal/sharing/rest/info", { @@ -205,15 +204,15 @@ describe("UserSession", () => { authInfo: { tokenServicesUrl: "https://pnp00035.esri.com/portal/sharing/rest/generateToken", - isTokenBasedSecurity: true - } + isTokenBasedSecurity: true, + }, }); fetchMock.postOnce( "https://pnp00035.esri.com/portal/sharing/rest/generateToken", { token: "new-server-token", - expires: TOMORROW + expires: TOMORROW, } ); @@ -223,27 +222,27 @@ describe("UserSession", () => { .getToken( "https://PNP00035.esri.com/server/rest/services/Hosted/perimeters_dd83/FeatureServer" ) - .then(token => { + .then((token) => { expect(token).toBe("new-server-token"); return session.getToken( "https://pnp00035.esri.com/server/rest/services/Hosted/otherService/FeatureServer" ); }) - .then(token => { + .then((token) => { expect(token).toBe("new-server-token"); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should fetch new tokens when tokens for trusted arcgis.com domains are expired", done => { + it("should fetch new tokens when tokens for trusted arcgis.com domains are expired", (done) => { const session = new UserSession({ clientId: "id", token: "token", refreshToken: "refresh", - tokenExpires: YESTERDAY + tokenExpires: YESTERDAY, }); fetchMock.mock( @@ -251,7 +250,7 @@ describe("UserSession", () => { { access_token: "new", expires_in: 1800, - username: "c@sey" + username: "c@sey", }, { repeat: 2, method: "POST" } ); @@ -260,41 +259,41 @@ describe("UserSession", () => { session.getToken("https://www.arcgis.com/sharing/rest/portals/self"), session.getToken( "https://services1.arcgis.com/MOCK_ORG/arcgis/rest/services/Private_Service/FeatureServer" - ) + ), ]) .then(([token1, token2]) => { expect(token1).toBe("new"); expect(token2).toBe("new"); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should pass through a token when no token expiration is present", done => { + it("should pass through a token when no token expiration is present", (done) => { const session = new UserSession({ - token: "token" + token: "token", }); session .getToken("https://www.arcgis.com/sharing/rest/portals/self") - .then(token1 => { + .then((token1) => { expect(token1).toBe("token"); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should generate a token for an untrusted, federated server", done => { + it("should generate a token for an untrusted, federated server", (done) => { const session = new UserSession({ clientId: "id", token: "token", refreshToken: "refresh", tokenExpires: TOMORROW, - portal: "https://gis.city.gov/sharing/rest" + portal: "https://gis.city.gov/sharing/rest", }); fetchMock.postOnce("https://gisservices.city.gov/public/rest/info", { @@ -303,49 +302,49 @@ describe("UserSession", () => { owningSystemUrl: "https://gis.city.gov", authInfo: { isTokenBasedSecurity: true, - tokenServicesUrl: "https://gis.city.gov/sharing/generateToken" - } + tokenServicesUrl: "https://gis.city.gov/sharing/generateToken", + }, }); fetchMock.postOnce("https://gis.city.gov/sharing/rest/info", { owningSystemUrl: "http://gis.city.gov", authInfo: { tokenServicesUrl: "https://gis.city.gov/sharing/generateToken", - isTokenBasedSecurity: true - } + isTokenBasedSecurity: true, + }, }); fetchMock.postOnce("https://gis.city.gov/sharing/generateToken", { token: "serverToken", - expires: TOMORROW + expires: TOMORROW, }); session .getToken( "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query" ) - .then(token => { + .then((token) => { expect(token).toBe("serverToken"); return session.getToken( "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query" ); }) - .then(token => { + .then((token) => { expect(token).toBe("serverToken"); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should generate a token for an untrusted, federated server admin call", done => { + it("should generate a token for an untrusted, federated server admin call", (done) => { const session = new UserSession({ clientId: "id", token: "token", refreshToken: "refresh", tokenExpires: TOMORROW, - portal: "https://gis.city.gov/sharing/rest" + portal: "https://gis.city.gov/sharing/rest", }); fetchMock.postOnce("https://gisservices.city.gov/public/rest/info", { @@ -354,47 +353,47 @@ describe("UserSession", () => { owningSystemUrl: "https://gis.city.gov", authInfo: { isTokenBasedSecurity: true, - tokenServicesUrl: "https://gis.city.gov/sharing/generateToken" - } + tokenServicesUrl: "https://gis.city.gov/sharing/generateToken", + }, }); fetchMock.postOnce("https://gis.city.gov/sharing/rest/info", { owningSystemUrl: "http://gis.city.gov", authInfo: { tokenServicesUrl: "https://gis.city.gov/sharing/generateToken", - isTokenBasedSecurity: true - } + isTokenBasedSecurity: true, + }, }); fetchMock.postOnce("https://gis.city.gov/sharing/generateToken", { token: "serverToken", - expires: TOMORROW + expires: TOMORROW, }); session .getToken( "https://gisservices.city.gov/public/rest/admin/services/trees/FeatureServer/addToDefinition" ) - .then(token => { + .then((token) => { expect(token).toBe("serverToken"); return session.getToken( "https://gisservices.city.gov/public/rest/admin/services/trees/FeatureServer/addToDefinition" ); }) - .then(token => { + .then((token) => { expect(token).toBe("serverToken"); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should generate a token for an untrusted, federated server with user credentials", done => { + it("should generate a token for an untrusted, federated server with user credentials", (done) => { const session = new UserSession({ username: "c@sey", password: "jones", - portal: "https://gis.city.gov/sharing/rest" + portal: "https://gis.city.gov/sharing/rest", }); fetchMock.postOnce("https://gisservices.city.gov/public/rest/info", { @@ -403,43 +402,43 @@ describe("UserSession", () => { owningSystemUrl: "https://gis.city.gov", authInfo: { isTokenBasedSecurity: true, - tokenServicesUrl: "https://gis.city.gov/sharing/generateToken" - } + tokenServicesUrl: "https://gis.city.gov/sharing/generateToken", + }, }); fetchMock.postOnce("https://gis.city.gov/sharing/rest/info", { owningSystemUrl: "http://gis.city.gov", authInfo: { tokenServicesUrl: "https://gis.city.gov/sharing/generateToken", - isTokenBasedSecurity: true - } + isTokenBasedSecurity: true, + }, }); fetchMock.postOnce("https://gis.city.gov/sharing/generateToken", { token: "serverToken", - expires: TOMORROW + expires: TOMORROW, }); session .getToken( "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query" ) - .then(token => { + .then((token) => { expect(token).toBe("serverToken"); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should only make 1 token request to an untrusted portal for similar URLs", done => { + 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" + portal: "https://gis.city.gov/sharing/rest", }); fetchMock.mock( @@ -450,8 +449,8 @@ describe("UserSession", () => { owningSystemUrl: "https://gis.city.gov", authInfo: { isTokenBasedSecurity: true, - tokenServicesUrl: "https://gis.city.gov/sharing/generateToken" - } + tokenServicesUrl: "https://gis.city.gov/sharing/generateToken", + }, }, { repeat: 1, method: "POST" } ); @@ -462,8 +461,8 @@ describe("UserSession", () => { owningSystemUrl: "http://gis.city.gov", authInfo: { tokenServicesUrl: "https://gis.city.gov/sharing/generateToken", - isTokenBasedSecurity: true - } + isTokenBasedSecurity: true, + }, }, { repeat: 1, method: "POST" } ); @@ -472,7 +471,7 @@ describe("UserSession", () => { "https://gis.city.gov/sharing/generateToken", { token: "serverToken", - expires: TOMORROW + expires: TOMORROW, }, { repeat: 1, method: "POST" } ); @@ -483,7 +482,7 @@ describe("UserSession", () => { ), session.getToken( "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query" - ) + ), ]) .then(([token1, token2]) => { expect(token1).toBe("serverToken"); @@ -493,17 +492,17 @@ describe("UserSession", () => { ).toBe(1); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should throw an ArcGISAuthError when the owning system doesn't match", done => { + it("should throw an ArcGISAuthError when the owning system doesn't match", (done) => { const session = new UserSession({ clientId: "id", token: "token", refreshToken: "refresh", - tokenExpires: YESTERDAY + tokenExpires: YESTERDAY, }); fetchMock.post("https://gisservices.city.gov/public/rest/info", { @@ -512,15 +511,15 @@ describe("UserSession", () => { owningSystemUrl: "https://gis.city.gov", authInfo: { isTokenBasedSecurity: true, - tokenServicesUrl: "https://gis.city.gov/sharing/generateToken" - } + tokenServicesUrl: "https://gis.city.gov/sharing/generateToken", + }, }); session .getToken( "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query" ) - .catch(e => { + .catch((e) => { expect(e.name).toEqual(ErrorTypes.ArcGISAuthError); expect(e.code).toEqual("NOT_FEDERATED"); expect(e.message).toEqual( @@ -530,17 +529,17 @@ describe("UserSession", () => { }); }); - it("should throw a fully hydrated ArcGISAuthError when no owning system is advertised", done => { + it("should throw a fully hydrated ArcGISAuthError when no owning system is advertised", (done) => { const session = new UserSession({ clientId: "id", token: "token", refreshToken: "refresh", - tokenExpires: YESTERDAY + tokenExpires: YESTERDAY, }); fetchMock.post("https://gisservices.city.gov/public/rest/info", { currentVersion: 10.51, - fullVersion: "10.5.1.120" + fullVersion: "10.5.1.120", }); fetchMock.post( @@ -549,8 +548,8 @@ describe("UserSession", () => { error: { code: 499, message: "Token Required", - details: [] - } + details: [], + }, } ); @@ -559,10 +558,10 @@ describe("UserSession", () => { { authentication: session, params: { - foo: "bar" - } + foo: "bar", + }, } - ).catch(e => { + ).catch((e) => { expect(e.name).toEqual(ErrorTypes.ArcGISAuthError); expect(e.code).toEqual("NOT_FEDERATED"); expect(e.message).toEqual( @@ -576,23 +575,23 @@ describe("UserSession", () => { }); }); - it("should not throw an ArcGISAuthError when the unfederated service is public", done => { + it("should not throw an ArcGISAuthError when the unfederated service is public", (done) => { const session = new UserSession({ clientId: "id", token: "token", refreshToken: "refresh", - tokenExpires: YESTERDAY + tokenExpires: YESTERDAY, }); fetchMock.post("https://gisservices.city.gov/public/rest/info", { currentVersion: 10.51, - fullVersion: "10.5.1.120" + fullVersion: "10.5.1.120", }); fetchMock.post( "https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query", { - count: 123 + count: 123, } ); @@ -601,111 +600,111 @@ describe("UserSession", () => { { authentication: session, params: { - returnCount: true - } + returnCount: true, + }, } ) - .then(res => { + .then((res) => { expect(res.count).toEqual(123); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); }); describe(".refreshSession()", () => { - it("should refresh with a username and password if expired", done => { + it("should refresh with a username and password if expired", (done) => { const session = new UserSession({ username: "c@sey", - password: "123456" + password: "123456", }); fetchMock.postOnce("https://www.arcgis.com/sharing/rest/generateToken", { token: "token", expires: TOMORROW.getTime(), - username: " c@sey" + username: " c@sey", }); session .refreshSession() - .then(s => { + .then((s) => { expect(s.token).toBe("token"); expect(s.tokenExpires).toEqual(TOMORROW); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should refresh with an unexpired refresh token", done => { + it("should refresh with an unexpired refresh token", (done) => { const session = new UserSession({ clientId: "clientId", token: "token", username: "c@sey", refreshToken: "refreshToken", - refreshTokenExpires: TOMORROW + refreshTokenExpires: TOMORROW, }); fetchMock.postOnce("https://www.arcgis.com/sharing/rest/oauth2/token", { access_token: "newToken", expires_in: 60, - username: " c@sey" + username: " c@sey", }); session .refreshSession() - .then(s => { + .then((s) => { expect(s.token).toBe("newToken"); expect(s.tokenExpires.getTime()).toBeGreaterThan(Date.now()); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should refresh with an expired refresh token", done => { + it("should refresh with an expired refresh token", (done) => { const session = new UserSession({ clientId: "clientId", token: "token", username: "c@sey", refreshToken: "refreshToken", refreshTokenExpires: YESTERDAY, - redirectUri: "https://example-app.com/redirect-uri" + redirectUri: "https://example-app.com/redirect-uri", }); fetchMock.postOnce("https://www.arcgis.com/sharing/rest/oauth2/token", { access_token: "newToken", expires_in: 60, username: " c@sey", - refresh_token: "newRefreshToken" + refresh_token: "newRefreshToken", }); session .refreshSession() - .then(s => { + .then((s) => { expect(s.token).toBe("newToken"); expect(s.tokenExpires.getTime()).toBeGreaterThan(Date.now()); expect(s.refreshToken).toBe("newRefreshToken"); expect(s.refreshTokenExpires.getTime()).toBeGreaterThan(Date.now()); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should reject if we cannot refresh the token", done => { + it("should reject if we cannot refresh the token", (done) => { const session = new UserSession({ clientId: "clientId", token: "token", - username: "c@sey" + username: "c@sey", }); - session.refreshSession().catch(e => { + session.refreshSession().catch((e) => { expect(e instanceof ArcGISAuthError).toBeTruthy(); expect(e.name).toBe("ArcGISAuthError"); expect(e.message).toBe("Unable to refresh token."); @@ -713,12 +712,12 @@ describe("UserSession", () => { }); }); - it("should only make 1 token request to the portal for similar URLs", 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 + tokenExpires: YESTERDAY, }); fetchMock.mock( @@ -726,14 +725,14 @@ describe("UserSession", () => { { access_token: "new", expires_in: 1800, - username: "c@sey" + username: "c@sey", }, { repeat: 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") + session.getToken("https://www.arcgis.com/sharing/rest/portals/self"), ]) .then(([token1, token2]) => { expect(token1).toBe("new"); @@ -744,34 +743,34 @@ describe("UserSession", () => { ).toBe(1); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); }); describe(".beginOAuth2()", () => { - it("should authorize via a popup", done => { + it("should authorize via a popup", (done) => { const MockWindow: any = { - open: jasmine.createSpy("spy") + open: jasmine.createSpy("spy"), }; UserSession.beginOAuth2( { clientId: "clientId123", redirectUri: "http://example-app.com/redirect", - state: "abc123" + state: "abc123", }, MockWindow ) - .then(session => { + .then((session) => { expect(session.token).toBe("token"); expect(session.username).toBe("c@sey"); expect(session.ssl).toBe(true); expect(session.tokenExpires).toEqual(TOMORROW); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); @@ -787,24 +786,24 @@ describe("UserSession", () => { token: "token", expires: TOMORROW, username: "c@sey", - ssl: true + ssl: true, }) ); }); - it("should reject the promise if there is an error", done => { + it("should reject the promise if there is an error", (done) => { const MockWindow: any = { - open: jasmine.createSpy("spy") + open: jasmine.createSpy("spy"), }; UserSession.beginOAuth2( { clientId: "clientId123", redirectUri: "http://example-app.com/redirect", - locale: "fr" + locale: "fr", }, MockWindow - ).catch(e => { + ).catch((e) => { done(); }); @@ -817,7 +816,7 @@ describe("UserSession", () => { MockWindow.__ESRI_REST_AUTH_HANDLER_clientId123( JSON.stringify({ errorMessage: "unable to sign in", - error: "SIGN_IN_FAILED" + error: "SIGN_IN_FAILED", }) ); }); @@ -825,8 +824,8 @@ describe("UserSession", () => { it("should authorize in the same window/tab", () => { const MockWindow: any = { location: { - href: "" - } + href: "", + }, }; // https://github.com/palantir/tslint/issues/3056 @@ -834,7 +833,7 @@ describe("UserSession", () => { { clientId: "clientId123", redirectUri: "http://example-app.com/redirect", - popup: false + popup: false, }, MockWindow ); @@ -847,8 +846,8 @@ describe("UserSession", () => { it("should authorize using a social media provider", () => { const MockWindow: any = { location: { - href: "" - } + href: "", + }, }; // https://github.com/palantir/tslint/issues/3056 @@ -857,7 +856,7 @@ describe("UserSession", () => { clientId: "clientId123", redirectUri: "http://example-app.com/redirect", popup: false, - provider: "facebook" + provider: "facebook", }, MockWindow ); @@ -870,8 +869,8 @@ describe("UserSession", () => { it("should authorize using the other social media provider", () => { const MockWindow: any = { location: { - href: "" - } + href: "", + }, }; // https://github.com/palantir/tslint/issues/3056 @@ -880,7 +879,7 @@ describe("UserSession", () => { clientId: "clientId123", redirectUri: "http://example-app.com/redirect", popup: false, - provider: "google" + provider: "google", }, MockWindow ); @@ -896,17 +895,17 @@ describe("UserSession", () => { const MockWindow = { location: { hash: - "#access_token=token&expires_in=1209600&username=c%40sey&ssl=true&persist=true" + "#access_token=token&expires_in=1209600&username=c%40sey&ssl=true&persist=true", }, get parent() { return this; - } + }, }; const session = UserSession.completeOAuth2( { clientId: "clientId", - redirectUri: "https://example-app.com/redirect-uri" + redirectUri: "https://example-app.com/redirect-uri", }, MockWindow ); @@ -921,24 +920,24 @@ describe("UserSession", () => { const MockWindow = { location: { hash: - "#access_token=token&expires_in=1209600&username=c%40sey&persist=true" + "#access_token=token&expires_in=1209600&username=c%40sey&persist=true", }, get parent() { return this; - } + }, }; const session = UserSession.completeOAuth2( { clientId: "clientId", - redirectUri: "https://example-app.com/redirect-uri" + redirectUri: "https://example-app.com/redirect-uri", }, MockWindow ); expect(session.ssl).toBe(false); }); - it("should callback to create a new user session if finds a valid opener.parent", done => { + it("should callback to create a new user session if finds a valid opener.parent", (done) => { const MockWindow = { opener: { parent: { @@ -953,28 +952,28 @@ describe("UserSession", () => { expect(new Date(oauthInfo.expires).getTime()).toBeGreaterThan( Date.now() ); - } - } + }, + }, }, close() { done(); }, location: { hash: - "#access_token=token&expires_in=1209600&username=c%40sey&ssl=true" - } + "#access_token=token&expires_in=1209600&username=c%40sey&ssl=true", + }, }; UserSession.completeOAuth2( { clientId: "clientId", - redirectUri: "https://example-app.com/redirect-uri" + redirectUri: "https://example-app.com/redirect-uri", }, MockWindow ); }); - it("should callback to create a new user session if finds a valid opener (Iframe support)", done => { + it("should callback to create a new user session if finds a valid opener (Iframe support)", (done) => { const MockWindow = { opener: { __ESRI_REST_AUTH_HANDLER_clientId( @@ -988,27 +987,27 @@ describe("UserSession", () => { expect(new Date(oauthInfo.expires).getTime()).toBeGreaterThan( Date.now() ); - } + }, }, close() { done(); }, location: { hash: - "#access_token=token&expires_in=1209600&username=c%40sey&ssl=true" - } + "#access_token=token&expires_in=1209600&username=c%40sey&ssl=true", + }, }; UserSession.completeOAuth2( { clientId: "clientId", - redirectUri: "https://example-app.com/redirect-uri" + redirectUri: "https://example-app.com/redirect-uri", }, MockWindow ); }); - it("should callback to create a new user session if finds a valid parent", done => { + it("should callback to create a new user session if finds a valid parent", (done) => { const MockWindow = { parent: { __ESRI_REST_AUTH_HANDLER_clientId( @@ -1022,21 +1021,21 @@ describe("UserSession", () => { expect(new Date(oauthInfo.expires).getTime()).toBeGreaterThan( Date.now() ); - } + }, }, close() { done(); }, location: { hash: - "#access_token=token&expires_in=1209600&username=c%40sey&ssl=true" - } + "#access_token=token&expires_in=1209600&username=c%40sey&ssl=true", + }, }; UserSession.completeOAuth2( { clientId: "clientId", - redirectUri: "https://example-app.com/redirect-uri" + redirectUri: "https://example-app.com/redirect-uri", }, MockWindow ); @@ -1045,18 +1044,18 @@ describe("UserSession", () => { it("should throw an error from the authorization window", () => { const MockWindow = { location: { - hash: "#error=Invalid_Signin&error_description=Invalid_Signin" + hash: "#error=Invalid_Signin&error_description=Invalid_Signin", }, get parent() { return this; - } + }, }; expect(function() { UserSession.completeOAuth2( { clientId: "clientId", - redirectUri: "https://example-app.com/redirect-uri" + redirectUri: "https://example-app.com/redirect-uri", }, MockWindow ); @@ -1069,23 +1068,23 @@ describe("UserSession", () => { throw new Error( "This window isn't where auth started, but was opened from somewhere else via window.open() perhaps from another domain which would cause a cross domain error when read." ); - } + }, }; const MockWindow = { location: { - hash: "#error=Invalid_Signin&error_description=Invalid_Signin" + hash: "#error=Invalid_Signin&error_description=Invalid_Signin", }, get opener() { return MockParent; - } + }, }; expect(function() { UserSession.completeOAuth2( { clientId: "clientId", - redirectUri: "https://example-app.com/redirect-uri" + redirectUri: "https://example-app.com/redirect-uri", }, MockWindow ); @@ -1093,13 +1092,13 @@ describe("UserSession", () => { }); }); - describe('postmessage auth :: ', () => { + describe("postmessage auth :: ", () => { const MockWindow = { addEventListener: () => {}, removeEventListener: () => {}, parent: { - postMessage: () => {} - } + postMessage: () => {}, + }, }; const cred = { @@ -1107,190 +1106,270 @@ describe("UserSession", () => { server: "https://www.arcgis.com", ssl: false, token: "token", - userId: "jsmith" + userId: "jsmith", }; - it('.disablePostMessageAuth removes event listener', () => { - const removeSpy = spyOn(MockWindow, 'removeEventListener'); - const session = UserSession.fromCredential(cred) + it(".disablePostMessageAuth removes event listener", () => { + const removeSpy = spyOn(MockWindow, "removeEventListener"); + const session = UserSession.fromCredential(cred); session.disablePostMessageAuth(MockWindow); - expect(removeSpy.calls.count()).toBe(1, 'should call removeEventListener'); + expect(removeSpy.calls.count()).toBe( + 1, + "should call removeEventListener" + ); }); - it('.enablePostMessageAuth adds event listener', () => { - const addSpy = spyOn(MockWindow, 'addEventListener'); + it(".enablePostMessageAuth adds event listener", () => { + const addSpy = spyOn(MockWindow, "addEventListener"); const session = UserSession.fromCredential(cred); - session.enablePostMessageAuth(['https://storymaps.arcgis.com'], MockWindow); - expect(addSpy.calls.count()).toBe(1, 'should call addEventListener'); + session.enablePostMessageAuth( + ["https://storymaps.arcgis.com"], + MockWindow + ); + expect(addSpy.calls.count()).toBe(1, "should call addEventListener"); }); - it('.enablePostMessage handler returns credential to origin in list', () => { - // ok, this gets kinda gnarly... + it(".enablePostMessage handler returns credential to origin in list", () => { + // ok, this gets kinda gnarly... // create a mock window object // that will hold the passed in event handler so we can fire it manually const Win = { - _fn: (evt:any) => {}, - addEventListener (evt:any, fn:any) { + _fn: (evt: any) => {}, + addEventListener(evt: any, fn: any) { // enablePostMessageAuth passes in the handler, which is what we're actually testing Win._fn = fn; }, - removeEventListener () {}, - } + removeEventListener() {}, + }; // Create the session const session = UserSession.fromCredential(cred); // enable postMessageAuth allowing storymaps.arcgis.com to recieve creds - session.enablePostMessageAuth(['https://storymaps.arcgis.com'], Win); + session.enablePostMessageAuth(["https://storymaps.arcgis.com"], Win); // create an event object, with a matching origin // an a source.postMessage fn that we can spy on const event = { - origin: 'https://storymaps.arcgis.com', + origin: "https://storymaps.arcgis.com", source: { - postMessage (msg: any, origin: string) {} - } - } + postMessage(msg: any, origin: string) {}, + }, + }; // create the spy - const sourceSpy = spyOn(event.source, 'postMessage'); + const sourceSpy = spyOn(event.source, "postMessage"); // Now, fire the handler, simulating what happens when a postMessage event comes // from an embedded iframe Win._fn(event); // Expectations... - expect(sourceSpy.calls.count()).toBe(1, 'souce.postMessage should be called in handler'); + expect(sourceSpy.calls.count()).toBe( + 1, + "souce.postMessage should be called in handler" + ); const args = sourceSpy.calls.argsFor(0); - expect(args[0].type).toBe('arcgis:auth:credential', 'should send credential type'); - expect(args[0].credential.userId).toBe('jsmith', 'should send credential'); + expect(args[0].type).toBe( + "arcgis:auth:credential", + "should send credential type" + ); + expect(args[0].credential.userId).toBe( + "jsmith", + "should send credential" + ); // now the case where it's not a valid origin - event.origin = 'https://evil.com'; + event.origin = "https://evil.com"; Win._fn(event); - expect(sourceSpy.calls.count()).toBe(2, 'souce.postMessage should be called in handler'); + expect(sourceSpy.calls.count()).toBe( + 2, + "souce.postMessage should be called in handler" + ); const args2 = sourceSpy.calls.argsFor(1); - expect(args2[0].type).toBe('arcgis:auth:rejected', 'should send reject'); + expect(args2[0].type).toBe("arcgis:auth:rejected", "should send reject"); }); - it('.fromParent happy path', () => { + it(".fromParent happy path", () => { // create a mock window that will fire the handler const Win = { - _fn: (evt:any) => {}, - addEventListener (evt:any, fn:any) { + _fn: (evt: any) => {}, + addEventListener(evt: any, fn: any) { Win._fn = fn; }, - removeEventListener () {}, + removeEventListener() {}, parent: { - postMessage (msg: any, origin:string) { - Win._fn({origin: 'https://origin.com', data: {type: 'arcgis:auth:credential', credential: cred }}); - } - } - } + postMessage(msg: any, origin: string) { + Win._fn({ + origin: "https://origin.com", + data: { type: "arcgis:auth:credential", credential: cred }, + }); + }, + }, + }; - return UserSession.fromParent('https://origin.com', Win) - .then((session) => { - expect(session.username).toBe('jsmith', 'should use the cred wired throu the mock window'); - }); + return UserSession.fromParent("https://origin.com", Win).then( + (session) => { + expect(session.username).toBe( + "jsmith", + "should use the cred wired throu the mock window" + ); + } + ); }); - it('.fromParent ignores other messages, then intercepts the correct one', async () => { + it(".fromParent ignores other messages, then intercepts the correct one", async () => { // create a mock window that will fire the handler const Win = { - _fn: (evt:any) => {}, - addEventListener (evt:any, fn:any) { + _fn: (evt: any) => {}, + addEventListener(evt: any, fn: any) { Win._fn = fn; }, - removeEventListener () {}, + removeEventListener() {}, parent: { - postMessage (msg: any, origin:string) { + postMessage(msg: any, origin: string) { // fire one we intend to ignore - Win._fn({origin: 'https://notorigin.com', data: {type: 'other:random', foo: {bar:"baz"} }}); + Win._fn({ + origin: "https://notorigin.com", + data: { type: "other:random", foo: { bar: "baz" } }, + }); // fire a second we want to intercept - Win._fn({origin: 'https://origin.com', data: {type: 'arcgis:auth:credential', credential: cred }}); - } - } - } + Win._fn({ + origin: "https://origin.com", + data: { type: "arcgis:auth:credential", credential: cred }, + }); + }, + }, + }; - return UserSession.fromParent('https://origin.com', Win) - .then((resp) => { - expect(resp.username).toBe('jsmith', 'should use the cred wired throu the mock window'); + return UserSession.fromParent("https://origin.com", Win).then((resp) => { + expect(resp.username).toBe( + "jsmith", + "should use the cred wired throu the mock window" + ); }); }); - it('.fromParent rejects if invlid cred', () => { + it(".fromParent rejects if invlid cred", () => { // create a mock window that will fire the handler const Win = { - _fn: (evt:any) => {}, - addEventListener (evt:any, fn:any) { + _fn: (evt: any) => {}, + addEventListener(evt: any, fn: any) { Win._fn = fn; }, - removeEventListener () {}, + removeEventListener() {}, parent: { - postMessage (msg: any, origin:string) { - Win._fn({origin: 'https://origin.com', data: {type: 'arcgis:auth:credential', credential: {foo:"bar"} }}); - } - } - } + postMessage(msg: any, origin: string) { + Win._fn({ + origin: "https://origin.com", + data: { + type: "arcgis:auth:credential", + credential: { foo: "bar" }, + }, + }); + }, + }, + }; - return UserSession.fromParent('https://origin.com', Win) - .catch((err) => { - expect(err).toBeDefined('Should reject'); - }) + return UserSession.fromParent("https://origin.com", Win).catch((err) => { + expect(err).toBeDefined("Should reject"); + }); }); - it('.fromParent rejects if auth rejected', () => { + it(".fromParent rejects if auth rejected", () => { // create a mock window that will fire the handler const Win = { - _fn: (evt:any) => {}, - addEventListener (evt:any, fn:any) { + _fn: (evt: any) => {}, + addEventListener(evt: any, fn: any) { Win._fn = fn; }, - removeEventListener () {}, + removeEventListener() {}, parent: { - postMessage (msg: any, origin:string) { - Win._fn({origin: 'https://origin.com', data: {type: 'arcgis:auth:rejected', message: 'Rejected authentication request.'}}); - } - } - } + postMessage(msg: any, origin: string) { + Win._fn({ + origin: "https://origin.com", + data: { + type: "arcgis:auth:rejected", + message: "Rejected authentication request.", + }, + }); + }, + }, + }; - return UserSession.fromParent('https://origin.com', Win) - .catch((err) => { - expect(err).toBeDefined('Should reject'); - }) + return UserSession.fromParent("https://origin.com", Win).catch((err) => { + expect(err).toBeDefined("Should reject"); + }); }); - it('.fromParent rejects if auth unknown message', () => { + it(".fromParent rejects if auth unknown message", () => { // create a mock window that will fire the handler const Win = { - _fn: (evt:any) => {}, - addEventListener (evt:any, fn:any) { + _fn: (evt: any) => {}, + addEventListener(evt: any, fn: any) { Win._fn = fn; }, - removeEventListener () {}, + removeEventListener() {}, parent: { - postMessage (msg: any, origin:string) { - Win._fn({origin: 'https://origin.com', data: {type: 'arcgis:auth:other'}}); - } - } - } + postMessage(msg: any, origin: string) { + Win._fn({ + origin: "https://origin.com", + data: { type: "arcgis:auth:other" }, + }); + }, + }, + }; - return UserSession.fromParent('https://origin.com', Win) - .catch((err) => { - expect(err.message).toBe("Unknown message type.", 'Should reject'); - }) + return UserSession.fromParent("https://origin.com", Win).catch((err) => { + expect(err.message).toBe("Unknown message type.", "Should reject"); + }); }); + }); + describe("validateAppAccess: ", () => { + it("makes a request to /oauth2/validateAppAccess passing params", () => { + const VERIFYAPPACCESS_URL = + "https://www.arcgis.com/sharing/rest/oauth2/validateAppAccess"; + fetchMock.postOnce(VERIFYAPPACCESS_URL, { + valid: true, + viewOnlyUserTypeApp: false, + }); + const session = new UserSession({ + clientId: "clientId", + redirectUri: "https://example-app.com/redirect-uri", + token: "FAKE-TOKEN", + tokenExpires: TOMORROW, + refreshToken: "refreshToken", + refreshTokenExpires: TOMORROW, + refreshTokenTTL: 1440, + username: "jsmith", + password: "123456", + }); + return session + .validateAppAccess("abc123") + .then((response) => { + const [url, options]: [string, RequestInit] = fetchMock.lastCall( + VERIFYAPPACCESS_URL + ); + expect(url).toEqual(VERIFYAPPACCESS_URL); + expect(options.body).toContain("f=json"); + expect(options.body).toContain("token=FAKE-TOKEN"); + expect(options.body).toContain("client_id=abc123"); + expect(response.valid).toEqual(true); + expect(response.viewOnlyUserTypeApp).toBe(false); + }) + .catch((e) => fail(e)); + }); }); it("should throw an unknown error if the url has no error or access_token", () => { const MockWindow = { location: { - hash: "" + hash: "", }, get opener() { return this; - } + }, }; expect(function() { UserSession.completeOAuth2( { clientId: "clientId", - redirectUri: "https://example-app.com/redirect-uri" + redirectUri: "https://example-app.com/redirect-uri", }, MockWindow ); @@ -1298,7 +1377,7 @@ describe("UserSession", () => { }); describe(".authorize()", () => { - it("should redirect the request to the authorization page", done => { + it("should redirect the request to the authorization page", (done) => { const spy = jasmine.createSpy("spy"); const MockResponse: any = { writeHead: spy, @@ -1308,13 +1387,13 @@ describe("UserSession", () => { "https://arcgis.com/sharing/rest/oauth2/authorize?client_id=clientId&duration=20160&response_type=code&redirect_uri=https%3A%2F%2Fexample-app.com%2Fredirect-uri" ); done(); - } + }, }; UserSession.authorize( { clientId: "clientId", - redirectUri: "https://example-app.com/redirect-uri" + redirectUri: "https://example-app.com/redirect-uri", }, MockResponse ); @@ -1332,23 +1411,23 @@ describe("UserSession", () => { paramsSpy.calls.reset(); }); - it("should exchange an authorization code for a new UserSession", done => { + it("should exchange an authorization code for a new UserSession", (done) => { fetchMock.postOnce("https://www.arcgis.com/sharing/rest/oauth2/token", { access_token: "token", expires_in: 1800, refresh_token: "refreshToken", username: "Casey", - ssl: true + ssl: true, }); UserSession.exchangeAuthorizationCode( { clientId: "clientId", - redirectUri: "https://example-app.com/redirect-uri" + redirectUri: "https://example-app.com/redirect-uri", }, "code" ) - .then(session => { + .then((session) => { expect(session.token).toBe("token"); expect(session.tokenExpires.getTime()).toBeGreaterThan(Date.now()); expect(session.username).toBe("Casey"); @@ -1356,7 +1435,7 @@ describe("UserSession", () => { expect(session.ssl).toBe(true); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); @@ -1365,14 +1444,14 @@ describe("UserSession", () => { describe(".getUser()", () => { afterEach(fetchMock.restore); - it("should cache metadata about the user", done => { + it("should cache metadata about the user", (done) => { // we intentionally only mock one response fetchMock.once( "https://www.arcgis.com/sharing/rest/community/self?f=json&token=token", { username: "jsmith", fullName: "John Smith", - role: "org_publisher" + role: "org_publisher", } ); @@ -1385,36 +1464,36 @@ describe("UserSession", () => { refreshTokenExpires: TOMORROW, refreshTokenTTL: 1440, username: "jsmith", - password: "123456" + password: "123456", }); session .getUser() - .then(response => { + .then((response) => { expect(response.role).toEqual("org_publisher"); session .getUser() - .then(cachedResponse => { + .then((cachedResponse) => { expect(cachedResponse.fullName).toEqual("John Smith"); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should never make more then 1 request", done => { + it("should never make more then 1 request", (done) => { // we intentionally only mock one response fetchMock.once( "https://www.arcgis.com/sharing/rest/community/self?f=json&token=token", { username: "jsmith", fullName: "John Smith", - role: "org_publisher" + role: "org_publisher", } ); @@ -1427,14 +1506,14 @@ describe("UserSession", () => { refreshTokenExpires: TOMORROW, refreshTokenTTL: 1440, username: "jsmith", - password: "123456" + password: "123456", }); Promise.all([session.getUser(), session.getUser()]) .then(() => { done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); @@ -1443,53 +1522,53 @@ describe("UserSession", () => { describe(".getUsername()", () => { afterEach(fetchMock.restore); - it("should fetch the username via getUser()", done => { + it("should fetch the username via getUser()", (done) => { // we intentionally only mock one response fetchMock.once( "https://www.arcgis.com/sharing/rest/community/self?f=json&token=token", { - username: "jsmith" + username: "jsmith", } ); const session = new UserSession({ - token: "token" + token: "token", }); session .getUsername() - .then(response => { + .then((response) => { expect(response).toEqual("jsmith"); // also test getting it from the cache. session .getUsername() - .then(username => { + .then((username) => { done(); expect(username).toEqual("jsmith"); }) - .catch(e => { + .catch((e) => { fail(e); }); }) - .catch(e => { + .catch((e) => { fail(e); }); }); - it("should use a username if passed in the session", done => { + it("should use a username if passed in the session", (done) => { const session = new UserSession({ - username: "jsmith" + username: "jsmith", }); session .getUsername() - .then(response => { + .then((response) => { expect(response).toEqual("jsmith"); done(); }) - .catch(e => { + .catch((e) => { fail(e); }); }); @@ -1501,7 +1580,7 @@ describe("UserSession", () => { server: "https://www.arcgis.com", ssl: false, token: "token", - userId: "jsmith" + userId: "jsmith", }; const MOCK_USER_SESSION = new UserSession({ @@ -1514,7 +1593,7 @@ describe("UserSession", () => { refreshTokenExpires: TOMORROW, refreshTokenTTL: 1440, username: "jsmith", - password: "123456" + password: "123456", }); it("should create a credential object from a session", () => { @@ -1551,7 +1630,7 @@ describe("UserSession", () => { const session = new UserSession({ clientId: "id", token: "token", - tokenExpires: TOMORROW + tokenExpires: TOMORROW, }); const root = session.getServerRootUrl( @@ -1564,7 +1643,7 @@ describe("UserSession", () => { const session = new UserSession({ clientId: "id", token: "token", - tokenExpires: TOMORROW + tokenExpires: TOMORROW, }); const root = session.getServerRootUrl( @@ -1579,7 +1658,7 @@ describe("UserSession", () => { const session = new UserSession({ clientId: "id", token: "token", - tokenExpires: TOMORROW + tokenExpires: TOMORROW, }); const root = session.getServerRootUrl( @@ -1592,34 +1671,34 @@ describe("UserSession", () => { }); describe("non-federated server", () => { - it("shouldnt fetch a fresh token if the current one isn't expired.", done => { + it("shouldnt fetch a fresh token if the current one isn't expired.", (done) => { const MOCK_USER_SESSION = new UserSession({ username: "c@sey", password: "123456", token: "token", tokenExpires: TOMORROW, - server: "https://fakeserver.com/arcgis" + server: "https://fakeserver.com/arcgis", }); MOCK_USER_SESSION.getToken( "https://fakeserver.com/arcgis/rest/services/Fake/MapServer/" ) - .then(token => { + .then((token) => { expect(token).toBe("token"); done(); }) - .catch(err => { + .catch((err) => { fail(err); }); }); - it("should fetch a fresh token if the current one is expired.", done => { + it("should fetch a fresh token if the current one is expired.", (done) => { const MOCK_USER_SESSION = new UserSession({ username: "jsmith", password: "123456", token: "token", tokenExpires: YESTERDAY, - server: "https://fakeserver.com/arcgis" + server: "https://fakeserver.com/arcgis", }); fetchMock.postOnce("https://fakeserver.com/arcgis/rest/info", { @@ -1627,20 +1706,20 @@ describe("UserSession", () => { fullVersion: "10.6.1", authInfo: { isTokenBasedSecurity: true, - tokenServicesUrl: "https://fakeserver.com/arcgis/tokens/" - } + tokenServicesUrl: "https://fakeserver.com/arcgis/tokens/", + }, }); fetchMock.postOnce("https://fakeserver.com/arcgis/tokens/", { token: "fresh-token", expires: TOMORROW.getTime(), - username: " jsmith" + username: " jsmith", }); MOCK_USER_SESSION.getToken( "https://fakeserver.com/arcgis/rest/services/Fake/MapServer/" ) - .then(token => { + .then((token) => { expect(token).toBe("fresh-token"); const [url, options]: [string, RequestInit] = fetchMock.lastCall( "https://fakeserver.com/arcgis/tokens/" @@ -1652,18 +1731,18 @@ describe("UserSession", () => { expect(options.body).toContain("client=referer"); done(); }) - .catch(err => { + .catch((err) => { fail(err); }); }); - it("should trim down the server url if necessary.", done => { + it("should trim down the server url if necessary.", (done) => { const MOCK_USER_SESSION = new UserSession({ username: "jsmith", password: "123456", token: "token", tokenExpires: YESTERDAY, - server: "https://fakeserver.com/arcgis/rest/services/blah/" + server: "https://fakeserver.com/arcgis/rest/services/blah/", }); fetchMock.postOnce("https://fakeserver.com/arcgis/rest/info", { @@ -1671,52 +1750,52 @@ describe("UserSession", () => { fullVersion: "10.6.1", authInfo: { isTokenBasedSecurity: true, - tokenServicesUrl: "https://fakeserver.com/arcgis/tokens/" - } + tokenServicesUrl: "https://fakeserver.com/arcgis/tokens/", + }, }); fetchMock.postOnce("https://fakeserver.com/arcgis/tokens/", { token: "fresh-token", expires: TOMORROW.getTime(), - username: " jsmith" + username: " jsmith", }); MOCK_USER_SESSION.getToken( "https://fakeserver.com/arcgis/rest/services/Fake/MapServer/" ) - .then(token => { + .then((token) => { expect(token).toBe("fresh-token"); done(); }) - .catch(err => { + .catch((err) => { fail(err); }); }); - it("should throw an error if the server isnt trusted.", done => { + it("should throw an error if the server isnt trusted.", (done) => { fetchMock.postOnce("https://fakeserver2.com/arcgis/rest/info", { currentVersion: 10.61, fullVersion: "10.6.1", authInfo: { isTokenBasedSecurity: true, - tokenServicesUrl: "https://fakeserver2.com/arcgis/tokens/" - } + tokenServicesUrl: "https://fakeserver2.com/arcgis/tokens/", + }, }); const MOCK_USER_SESSION = new UserSession({ username: "c@sey", password: "123456", token: "token", tokenExpires: TOMORROW, - server: "https://fakeserver.com/arcgis" + server: "https://fakeserver.com/arcgis", }); MOCK_USER_SESSION.getToken( "https://fakeserver2.com/arcgis/rest/services/Fake/MapServer/" ) - .then(token => { + .then((token) => { fail(token); }) - .catch(err => { + .catch((err) => { expect(err.code).toBe("NOT_FEDERATED"); expect(err.originalMessage).toEqual( "https://fakeserver2.com/arcgis/rest/services/Fake/MapServer/ is not federated with any portal and is not explicitly trusted." diff --git a/packages/arcgis-rest-auth/test/app-tokens.test.ts b/packages/arcgis-rest-auth/test/app-tokens.test.ts new file mode 100644 index 0000000000..1ad8da65de --- /dev/null +++ b/packages/arcgis-rest-auth/test/app-tokens.test.ts @@ -0,0 +1,91 @@ +/* Copyright (c) 2018-2020 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import * as fetchMock from "fetch-mock"; + +import { exchangeToken, platformSelf } from "../src/app-tokens"; + +describe("app-token functions: ", () => { + describe("exchangeToken:", () => { + it("makes a request to /oauth2/exchangeToken passing params", () => { + const EXCHANGE_TOKEN_URL = + "https://www.arcgis.com/sharing/rest/oauth2/exchangeToken"; + fetchMock.postOnce(EXCHANGE_TOKEN_URL, { + token: "APP-TOKEN", + }); + return exchangeToken("FAKE-TOKEN", "CLIENT-ID-ABC123") + .then((response) => { + const [url, options]: [string, RequestInit] = fetchMock.lastCall( + EXCHANGE_TOKEN_URL + ); + expect(url).toEqual(EXCHANGE_TOKEN_URL); + expect(options.body).toContain("f=json"); + expect(options.body).toContain("token=FAKE-TOKEN"); + expect(options.body).toContain("client_id=CLIENT-ID-ABC123"); + expect(response).toEqual("APP-TOKEN"); + }) + .catch((e) => fail(e)); + }); + it("takes a portalUrl", () => { + const PORTAL_BASE_URL = "https://my-portal.com/instance/sharing/rest"; + const PORTAL_EXCHANGE_URL = `${PORTAL_BASE_URL}/oauth2/exchangeToken`; + fetchMock.postOnce(PORTAL_EXCHANGE_URL, { + valid: true, + viewOnlyUserTypeApp: false, + }); + return exchangeToken("FAKE-TOKEN", "CLIENT-ID-ABC123", PORTAL_BASE_URL) + .then((response) => { + const [url, options]: [string, RequestInit] = fetchMock.lastCall( + PORTAL_EXCHANGE_URL + ); + expect(url).toEqual(PORTAL_EXCHANGE_URL); + }) + .catch((e) => fail(e)); + }); + }); + + describe("platformSelf:", () => { + it("makes a request to /oauth2/platformSelf passing params", () => { + const PLATFORM_SELF_URL = + "https://www.arcgis.com/sharing/rest/oauth2/platformSelf"; + fetchMock.postOnce(PLATFORM_SELF_URL, { + username: "jsmith", + token: "APP-TOKEN", + }); + return platformSelf( + "CLIENT-ID-ABC123", + "https://hub.arcgis.com/torii-provider-arcgis/redirect.html" + ) + .then((response) => { + const [url, options]: [string, RequestInit] = fetchMock.lastCall( + PLATFORM_SELF_URL + ); + expect(url).toEqual(PLATFORM_SELF_URL); + const headers = options.headers || ({} as any); + expect(headers["X-Esri-Auth-Redirect-Uri"]).toBe( + "https://hub.arcgis.com/torii-provider-arcgis/redirect.html" + ); + expect(headers["X-Esri-Auth-Client-Id"]).toBe("CLIENT-ID-ABC123"); + expect(response.token).toEqual("APP-TOKEN"); + expect(response.username).toEqual("jsmith"); + }) + .catch((e) => fail(e)); + }); + it("takes a portalUrl", () => { + const PORTAL_BASE_URL = "https://my-portal.com/instance/sharing/rest"; + const PORTAL_PLATFORM_SELF_URL = `${PORTAL_BASE_URL}/oauth2/platformSelf`; + fetchMock.postOnce(PORTAL_PLATFORM_SELF_URL, { + username: "jsmith", + token: "APP-TOKEN", + }); + return platformSelf("FAKE-TOKEN", "CLIENT-ID-ABC123", PORTAL_BASE_URL) + .then((response) => { + const [url, options]: [string, RequestInit] = fetchMock.lastCall( + PORTAL_PLATFORM_SELF_URL + ); + expect(url).toEqual(PORTAL_PLATFORM_SELF_URL); + }) + .catch((e) => fail(e)); + }); + }); +}); diff --git a/packages/arcgis-rest-auth/test/validate-app-access.test.ts b/packages/arcgis-rest-auth/test/validate-app-access.test.ts new file mode 100644 index 0000000000..026a5d92a3 --- /dev/null +++ b/packages/arcgis-rest-auth/test/validate-app-access.test.ts @@ -0,0 +1,46 @@ +/* Copyright (c) 2018-2020 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ +import * as fetchMock from "fetch-mock"; + +import { validateAppAccess } from "../src/validate-app-access"; + +const VERIFYAPPACCESS_URL = + "https://www.arcgis.com/sharing/rest/oauth2/validateAppAccess"; + +describe("validateAppAccess: ", () => { + it("makes a request to /oauth2/validateAppAccess passing params", () => { + fetchMock.postOnce(VERIFYAPPACCESS_URL, { + valid: true, + viewOnlyUserTypeApp: false, + }); + return validateAppAccess("FAKE-TOKEN", "abc123") + .then((response) => { + const [url, options]: [string, RequestInit] = fetchMock.lastCall( + VERIFYAPPACCESS_URL + ); + expect(url).toEqual(VERIFYAPPACCESS_URL); + expect(options.body).toContain("f=json"); + expect(options.body).toContain("token=FAKE-TOKEN"); + expect(options.body).toContain("client_id=abc123"); + expect(response.valid).toEqual(true); + expect(response.viewOnlyUserTypeApp).toBe(false); + }) + .catch((e) => fail(e)); + }); + it("takes a portalUrl", () => { + const PORTAL_BASE_URL = "https://my-portal.com/instance/sharing/rest"; + const PORTAL_VERIFY_URL = `${PORTAL_BASE_URL}/oauth2/validateAppAccess`; + fetchMock.postOnce(PORTAL_VERIFY_URL, { + valid: true, + viewOnlyUserTypeApp: false, + }); + return validateAppAccess("FAKE-TOKEN", "abc123", PORTAL_BASE_URL) + .then((response) => { + const [url, options]: [string, RequestInit] = fetchMock.lastCall( + PORTAL_VERIFY_URL + ); + expect(url).toEqual(PORTAL_VERIFY_URL); + }) + .catch((e) => fail(e)); + }); +});