Skip to content

Commit

Permalink
fix(arcgis-rest-auth): postMessage internals
Browse files Browse the repository at this point in the history
ensure event.source === window.parent
no response unless origin and message type are expected

update guide and spec

AFFECTS PACKAGES:
@esri/arcgis-rest-auth
  • Loading branch information
dbouwman committed Feb 4, 2021
1 parent 75b4908 commit bd8eb6c
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 37 deletions.
16 changes: 7 additions & 9 deletions docs/src/guides/embedded-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ group: 2-authentication

# Authentication with Embedded Applications

Sometimes an application will need to embed another application using an `<iframe>`. If both applications are backed by items that are publicly accessible, things will just work.
Sometimes an application will need to embed another application using an `<iframe>`. If both applications are backed by items that are publicly accessible, things will "just work".

However, if the embedded application is not public and the user has already logged into the "Host" application, we then run into the question of how to pass authentication from the "Host" to the embedded application.

Expand All @@ -22,7 +22,7 @@ Although this is internalized within the functions, the message types and the ob

Cross-Origin embedding occurs when the "host" app and the "embedded" application are served from different locations. This is only supported for ArcGIS Platform apps that support embedding.

For example, you can build a custom app, hosted at `http://myapp.com` and iframe in a "platform app" that supports embedding. However, you can not embed your custom app into a storymap, and expect the storymap to pass authentication to your app. This is done for security reasons.
For example, you can build a custom app, hosted at `http://myapp.com` and iframe in a "platform app" that supports embedding. However, you can not embed your custom app into a StoryMap, and expect the StoryMap to pass authentication to your app. This is done for security reasons.

## Using `postMessage`

Expand Down Expand Up @@ -63,11 +63,9 @@ Let's suppose the host app is embedding `https://storymaps.arcgis.com/stories/15
```js
const originalUrl =
"https://storymaps.arcgis.com/stories/15a9b9991fff47ad84f4618a28b01afd";
const embedUrl = `${originalurl}?arcgis-auth-origin=${encodeURIComponent(
window.location.origin
)}&arcgis-auth-portal=${encodeURIComponent(
session.portal
)}`;
const embedUrl = `${originalurl}
?arcgis-auth-origin=${encodeURIComponent(window.location.origin)}
&arcgis-auth-portal=${encodeURIComponent(session.portal)}`;
// then use embedUrl in your component that renders the <iframe>
```

Expand All @@ -87,8 +85,8 @@ if (arcgisAuthOrigin) {
// the embeded app should exchange this token for one specific to the application
})
.catch((ex) => {
// if the origin of the embedded app is not in the parent's validOrigin array
// this will throw with a message "Rejected authentication request."
// The only case it will reject is if the parent is unable to return a credential
// if the parent does not see the child as a valid origin, the parent will never respond.
});
}
```
Expand Down
10 changes: 9 additions & 1 deletion packages/arcgis-rest-auth/post-message-auth-spec.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Post Message Authentication Specification

The general idea is that the HOST application will only respond when the following conditions are true:

- the event.source.origin is a location that the HOST trusts
- the message type is `arcgis:auth:requestCredential`

Under any other conditions, the HOST application will not send any response.

## Message Types

Messages send via `postMessage` can be any object, but by convention usually have a `type` property that describes what sort of message it is.
Expand Down Expand Up @@ -47,7 +54,8 @@ Message Object

## `arcgis:auth:rejected`

Sent from an host app, to an embedded app, with an error indicating why
Sent from an host app, to an embedded app, with an error indicating why.
This will only be sent if the HOST app has some issue getting the credential.

Message Object

Expand Down
49 changes: 29 additions & 20 deletions packages/arcgis-rest-auth/src/UserSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,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: "",
},
Expand Down Expand Up @@ -372,11 +373,7 @@ export class UserSession implements IAuthenticationManager {
}
};

win.open(
url,
"oauth-window",
popupWindowFeatures
);
win.open(url, "oauth-window", popupWindowFeatures);

return session.promise;
}
Expand Down Expand Up @@ -479,7 +476,15 @@ export class UserSession implements IAuthenticationManager {
* 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. This function wraps
* that behavior.
*
* The ArcGIS API for Javascript has this built into the Identity Manager as of the 4.19 release.
*
* Note: The parent application will not respond if the embedded app's origin is not:
* - the same origin as the parent or *.arcgis.com (JSAPI)
* - in the list of valid child origins (REST-JS)
*
*
* @param parentOrigin origin of the parent frame. Passed into the embedded application as `parentOrigin` query param
* @browserOnly
Expand All @@ -496,9 +501,8 @@ export class UserSession implements IAuthenticationManager {
return new Promise((resolve, reject) => {
// create an event handler that just wraps the parentMessageHandler
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) {
// ensure we only listen to events from the parent
if (event.source === win.parent && event.data) {
try {
return resolve(UserSession.parentMessageHandler(event));
} catch (err) {
Expand Down Expand Up @@ -635,8 +639,8 @@ export class UserSession implements IAuthenticationManager {
if (event.data.type === "arcgis:auth:credential") {
return UserSession.fromCredential(event.data.credential);
}
if (event.data.type === "arcgis:auth:rejected") {
throw new Error(event.data.message);
if (event.data.type === "arcgis:auth:error") {
throw new Error(event.data.error);
} else {
throw new Error("Unknown message type.");
}
Expand Down Expand Up @@ -969,20 +973,25 @@ export class UserSession implements IAuthenticationManager {
// return a function that closes over the validOrigins and
// has access to the credential
return (event: any) => {
// Verify that the origin is valid
// 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 isValidOrigin = validOrigins.indexOf(event.origin) > -1;
// JSAPI handles this slightly differently - instead of checking a list, it will respond if
// event.origin === window.location.origin || event.origin.endsWith('.arcgis.com')
// For Hub, and to enable cross domain debugging with port's in urls, we are opting to
// use a list of valid origins

// Ensure the message type is something we want to handle
const isValidType = event.data.type === "arcgis:auth:requestCredential";

if (isValidOrigin && isValidType) {
const credential = this.toCredential();
event.source.postMessage(
{ type: "arcgis:auth:credential", credential },
event.origin
);
} else {
event.source.postMessage(
{
type: "arcgis:auth:rejected",
message: `Rejected authentication request.`,
type: "arcgis:auth:credential",
credential,
},
event.origin
);
Expand Down
21 changes: 14 additions & 7 deletions packages/arcgis-rest-auth/test/UserSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,9 @@ describe("UserSession", () => {
source: {
postMessage(msg: any, origin: string) {},
},
data: {
type: "arcgis:auth:requestCredential",
},
};
// create the spy
const sourceSpy = spyOn(event.source, "postMessage");
Expand All @@ -1176,11 +1179,9 @@ describe("UserSession", () => {
event.origin = "https://evil.com";
Win._fn(event);
expect(sourceSpy.calls.count()).toBe(
2,
"souce.postMessage should be called in handler"
1,
"souce.postMessage should not be called in handler for invalid origin"
);
const args2 = sourceSpy.calls.argsFor(1);
expect(args2[0].type).toBe("arcgis:auth:rejected", "should send reject");
});

it(".fromParent happy path", () => {
Expand All @@ -1196,6 +1197,7 @@ describe("UserSession", () => {
Win._fn({
origin: "https://origin.com",
data: { type: "arcgis:auth:credential", credential: cred },
source: Win.parent,
});
},
},
Expand Down Expand Up @@ -1225,11 +1227,13 @@ describe("UserSession", () => {
Win._fn({
origin: "https://notorigin.com",
data: { type: "other:random", foo: { bar: "baz" } },
source: "Not Parent Object",
});
// fire a second we want to intercept
Win._fn({
origin: "https://origin.com",
data: { type: "arcgis:auth:credential", credential: cred },
source: Win.parent,
});
},
},
Expand Down Expand Up @@ -1259,6 +1263,7 @@ describe("UserSession", () => {
type: "arcgis:auth:credential",
credential: { foo: "bar" },
},
source: Win.parent,
});
},
},
Expand All @@ -1269,7 +1274,7 @@ describe("UserSession", () => {
});
});

it(".fromParent rejects if auth rejected", () => {
it(".fromParent rejects if auth error recieved", () => {
// create a mock window that will fire the handler
const Win = {
_fn: (evt: any) => {},
Expand All @@ -1282,9 +1287,10 @@ describe("UserSession", () => {
Win._fn({
origin: "https://origin.com",
data: {
type: "arcgis:auth:rejected",
message: "Rejected authentication request.",
type: "arcgis:auth:error",
error: { message: "Rejected authentication request." },
},
source: Win.parent,
});
},
},
Expand All @@ -1308,6 +1314,7 @@ describe("UserSession", () => {
Win._fn({
origin: "https://origin.com",
data: { type: "arcgis:auth:other" },
source: Win.parent,
});
},
},
Expand Down

0 comments on commit bd8eb6c

Please sign in to comment.