Skip to content

Commit

Permalink
💅 code review
Browse files Browse the repository at this point in the history
  • Loading branch information
jgravois committed Jan 17, 2019
1 parent e88d75e commit 34acd33
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 15 deletions.
60 changes: 47 additions & 13 deletions packages/arcgis-rest-auth/src/UserSession.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
/* Copyright (c) 2017-2018 Environmental Systems Research Institute, Inc.
/* Copyright (c) 2017-2019 Environmental Systems Research Institute, Inc.
* Apache-2.0 */

/**
* notes:
* /generateToken returns a token that cannot be refreshed.
*
* oauth2/token can return a token *and* a refreshToken.
* until the refreshToken expires, you can use it (and a clientId)
* to fetch a fresh token without a username and password.
*
* the catch is that this flow, called 'authorization_code' is only utilized
* by server based OAuth 2 Node.js applications that call /authorize first.
*/

import * as http from "http";
import {
request,
Expand Down Expand Up @@ -100,6 +112,11 @@ export interface IOauth2Options {
*/
refreshTokenTTL?: number;

/**
* An unfederated ArcGIS Server instance that recognizes the supplied credentials.
*/
server?: string;

/**
* The locale assumed to render the login page.
*
Expand Down Expand Up @@ -601,8 +618,18 @@ export class UserSession implements IAuthenticationManager {
this.tokenDuration = options.tokenDuration || 20160;
this.redirectUri = options.redirectUri;
this.refreshTokenTTL = options.refreshTokenTTL || 1440;

this.trustedServers = {};
// 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 [serverRoot] = options.server
.toLowerCase()
.split(/\/rest(\/admin)?\/services\//);
this.trustedServers[serverRoot] = {
token: options.token,
expires: options.tokenExpires
};
}
this._pendingTokenRequests = {};
}

Expand Down Expand Up @@ -730,7 +757,11 @@ export class UserSession implements IAuthenticationManager {
const [root] = url.toLowerCase().split(/\/rest(\/admin)?\/services\//);
const existingToken = this.trustedServers[root];

if (existingToken && existingToken.expires.getTime() > Date.now()) {
if (
existingToken &&
existingToken.expires &&
existingToken.expires.getTime() > Date.now()
) {
return Promise.resolve(existingToken.token);
}

Expand All @@ -751,10 +782,13 @@ export class UserSession implements IAuthenticationManager {
*/
if (!new RegExp(response.owningSystemUrl, "i").test(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"
);
} else {
/**
* if the server is federated, use the relevant token endpoint.
*/
return request(
`${response.owningSystemUrl}/sharing/rest/info`,
requestOptions
Expand All @@ -766,14 +800,12 @@ export class UserSession implements IAuthenticationManager {
) {
/**
* if its a stand-alone instance of ArcGIS Server that doesn't advertise
* federation at all and the root url is recognized, use its built in token endpoint.
* federation, but the root server url is recognized, use its built in token endpoint.
*/
return new Promise((resolve, reject) => {
resolve({ authInfo: response.authInfo });
});
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"
);
}
Expand All @@ -782,15 +814,17 @@ export class UserSession implements IAuthenticationManager {
return response.authInfo.tokenServicesUrl;
})
.then((tokenServicesUrl: string) => {
if (this.token) {
// 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
expiration: this.tokenDuration,
client: "referer"
}
});
// generate an entirely fresh token if necessary
// generate an entirely fresh token from scratch if necessary
} else {
return generateToken(tokenServicesUrl, {
params: {
Expand Down Expand Up @@ -893,7 +927,7 @@ export class UserSession implements IAuthenticationManager {
}

/**
* Exchanges an expired `refreshToken` for a new one also updates `token` and
* Exchanges an unexpired `refreshToken` for a new one, also updates `token` and
* `tokenExpires`.
*/
private refreshRefreshToken(requestOptions?: ITokenRequestOptions) {
Expand Down
140 changes: 138 additions & 2 deletions packages/arcgis-rest-auth/test/UserSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "@esri/arcgis-rest-request";
import * as fetchMock from "fetch-mock";
import { YESTERDAY, TOMORROW } from "./utils";
import { doesNotThrow } from "assert";

describe("UserSession", () => {
afterEach(fetchMock.restore);
Expand Down Expand Up @@ -471,7 +472,7 @@ describe("UserSession", () => {
expect(e.name).toEqual(ErrorTypes.ArcGISAuthError);
expect(e.code).toEqual("NOT_FEDERATED");
expect(e.message).toEqual(
"NOT_FEDERATED: https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query is not federated with https://www.arcgis.com/sharing/rest."
"NOT_FEDERATED: https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query is not federated with any portal and is not explicitly trusted."
);
done();
});
Expand Down Expand Up @@ -502,7 +503,7 @@ describe("UserSession", () => {
expect(e.name).toEqual(ErrorTypes.ArcGISAuthError);
expect(e.code).toEqual("NOT_FEDERATED");
expect(e.message).toEqual(
"NOT_FEDERATED: https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query is not federated with https://www.arcgis.com/sharing/rest."
"NOT_FEDERATED: https://gisservices.city.gov/public/rest/services/trees/FeatureServer/0/query is not federated with any portal and is not explicitly trusted."
);
done();
});
Expand Down Expand Up @@ -1086,4 +1087,139 @@ describe("UserSession", () => {
expect(credSession.tokenExpires).toEqual(new Date(TOMORROW));
});
});

describe("non-federated server", () => {
it("shouldnt fetch a fresh token if the current one isnt expired.", done => {
const MOCK_USER_SESSION = new UserSession({
username: "c@sey",
password: "123456",
token: "token",
tokenExpires: TOMORROW,
server: "https://fakeserver.com/arcgis"
});

MOCK_USER_SESSION.getToken(
"https://fakeserver.com/arcgis/rest/services/Fake/MapServer/"
)
.then(token => {
expect(token).toBe("token");
done();
})
.catch(err => {
fail(err);
});
});

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"
});

fetchMock.postOnce("https://fakeserver.com/arcgis/rest/info", {
currentVersion: 10.61,
fullVersion: "10.6.1",
authInfo: {
isTokenBasedSecurity: true,
tokenServicesUrl: "https://fakeserver.com/arcgis/tokens/"
}
});

fetchMock.postOnce("https://fakeserver.com/arcgis/tokens/", {
token: "fresh-token",
expires: TOMORROW.getTime(),
username: " jsmith"
});

MOCK_USER_SESSION.getToken(
"https://fakeserver.com/arcgis/rest/services/Fake/MapServer/"
)
.then(token => {
expect(token).toBe("fresh-token");
const [url, options]: [string, RequestInit] = fetchMock.lastCall(
"https://fakeserver.com/arcgis/tokens/"
);
expect(options.method).toBe("POST");
expect(options.body).toContain("f=json");
expect(options.body).toContain("username=jsmith");
expect(options.body).toContain("password=123456");
expect(options.body).toContain("client=referer");
done();
})
.catch(err => {
fail(err);
});
});

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/"
});

fetchMock.postOnce("https://fakeserver.com/arcgis/rest/info", {
currentVersion: 10.61,
fullVersion: "10.6.1",
authInfo: {
isTokenBasedSecurity: true,
tokenServicesUrl: "https://fakeserver.com/arcgis/tokens/"
}
});

fetchMock.postOnce("https://fakeserver.com/arcgis/tokens/", {
token: "fresh-token",
expires: TOMORROW.getTime(),
username: " jsmith"
});

MOCK_USER_SESSION.getToken(
"https://fakeserver.com/arcgis/rest/services/Fake/MapServer/"
)
.then(token => {
expect(token).toBe("fresh-token");
done();
})
.catch(err => {
fail(err);
});
});

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/"
}
});
const MOCK_USER_SESSION = new UserSession({
username: "c@sey",
password: "123456",
token: "token",
tokenExpires: TOMORROW,
server: "https://fakeserver.com/arcgis"
});

MOCK_USER_SESSION.getToken(
"https://fakeserver2.com/arcgis/rest/services/Fake/MapServer/"
)
.then(token => {
fail(token);
})
.catch(err => {
expect(err.code).toBe("NOT_FEDERATED");
expect(err.originalMessage).toBe(
"https://fakeserver2.com/arcgis/rest/services/Fake/MapServer/ is not federated with any portal and is not explicitly trusted."
);
done();
});
});
});
});

0 comments on commit 34acd33

Please sign in to comment.