Skip to content

Commit

Permalink
chore(embedded): refresh the guest token (#19132)
Browse files Browse the repository at this point in the history
* refresh the guest token

* put back the date logic

* version

* fix time hijinks

* test

* Update superset-embedded-sdk/src/guestTokenRefresh.ts
  • Loading branch information
suddjian authored Mar 14, 2022
1 parent 76b4a14 commit 54b60de
Show file tree
Hide file tree
Showing 11 changed files with 10,705 additions and 3,192 deletions.
1 change: 1 addition & 0 deletions .github/workflows/embedded-sdk-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ jobs:
node-version: "16"
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm test
- run: npm run build
13,729 changes: 10,549 additions & 3,180 deletions superset-embedded-sdk/package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions superset-embedded-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@superset-ui/embedded-sdk",
"version": "0.1.0-alpha.4",
"version": "0.1.0-alpha.5",
"description": "SDK for embedding resources from Superset into your own application",
"access": "public",
"keywords": [
Expand All @@ -24,7 +24,7 @@
"scripts": {
"build": "tsc ; babel src --out-dir lib --extensions '.ts,.tsx' ; webpack --mode production",
"ci:release": "node ./release-if-necessary.js",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "jest"
},
"browserslist": [
"last 3 chrome versions",
Expand All @@ -40,8 +40,10 @@
"@babel/core": "^7.16.12",
"@babel/preset-env": "^7.16.11",
"@babel/preset-typescript": "^7.16.7",
"@types/jest": "^27.4.1",
"axios": "^0.25.0",
"babel-loader": "^8.2.3",
"jest": "^27.5.1",
"typescript": "^4.5.5",
"webpack": "^5.67.0",
"webpack-cli": "^4.9.2"
Expand Down
96 changes: 96 additions & 0 deletions superset-embedded-sdk/src/guestTokenRefresh.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import {
REFRESH_TIMING_BUFFER_MS,
getGuestTokenRefreshTiming,
MIN_REFRESH_WAIT_MS,
DEFAULT_TOKEN_EXP_MS,
} from "./guestTokenRefresh";

describe("guest token refresh", () => {
beforeAll(() => {
jest.useFakeTimers("modern"); // "modern" allows us to fake the system time
jest.setSystemTime(new Date("2022-03-03 01:00"));
jest.spyOn(global, "setTimeout");
});

afterAll(() => {
jest.useRealTimers();
});

function makeFakeJWT(claims: any) {
// not a valid jwt, but close enough for this code
const tokenifiedClaims = Buffer.from(JSON.stringify(claims)).toString(
"base64"
);
return `abc.${tokenifiedClaims}.xyz`;
}

it("schedules refresh with an epoch exp", () => {
// exp is in seconds
const ttl = 1300;
const exp = Date.now() / 1000 + ttl;
const fakeToken = makeFakeJWT({ exp });

const timing = getGuestTokenRefreshTiming(fakeToken);

expect(timing).toBeGreaterThan(MIN_REFRESH_WAIT_MS);
expect(timing).toBe(ttl * 1000 - REFRESH_TIMING_BUFFER_MS);
});

it("schedules refresh with an epoch exp containing a decimal", () => {
const ttl = 1300.123;
const exp = Date.now() / 1000 + ttl;
const fakeToken = makeFakeJWT({ exp });

const timing = getGuestTokenRefreshTiming(fakeToken);

expect(timing).toBeGreaterThan(MIN_REFRESH_WAIT_MS);
expect(timing).toBe(ttl * 1000 - REFRESH_TIMING_BUFFER_MS);
});

it("schedules refresh with iso exp", () => {
const exp = new Date("2022-03-03 01:09").toISOString();
const fakeToken = makeFakeJWT({ exp });

const timing = getGuestTokenRefreshTiming(fakeToken);
const expectedTiming = 1000 * 60 * 9 - REFRESH_TIMING_BUFFER_MS;

expect(timing).toBeGreaterThan(MIN_REFRESH_WAIT_MS);
expect(timing).toBe(expectedTiming);
});

it("avoids refresh spam", () => {
const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 });

const timing = getGuestTokenRefreshTiming(fakeToken);

expect(timing).toBe(MIN_REFRESH_WAIT_MS - REFRESH_TIMING_BUFFER_MS);
});

it("uses a default when it cannot parse the date", () => {
const fakeToken = makeFakeJWT({ exp: "invalid date" });

const timing = getGuestTokenRefreshTiming(fakeToken);

expect(timing).toBeGreaterThan(MIN_REFRESH_WAIT_MS);
expect(timing).toBe(DEFAULT_TOKEN_EXP_MS - REFRESH_TIMING_BUFFER_MS);
});
});
32 changes: 32 additions & 0 deletions superset-embedded-sdk/src/guestTokenRefresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export const REFRESH_TIMING_BUFFER_MS = 5000 // refresh guest token early to avoid failed superset requests
export const MIN_REFRESH_WAIT_MS = 10000 // avoid blasting requests as fast as the cpu can handle
export const DEFAULT_TOKEN_EXP_MS = 300000 // (5 min) used only when parsing guest token exp fails

// when do we refresh the guest token?
export function getGuestTokenRefreshTiming(currentGuestToken: string) {
const parsedJwt = JSON.parse(Buffer.from(currentGuestToken.split('.')[1], 'base64').toString());
// if exp is int, it is in seconds, but Date() takes milliseconds
const exp = new Date(/[^0-9\.]/g.test(parsedJwt.exp) ? parsedJwt.exp : parseFloat(parsedJwt.exp) * 1000);
const isValidDate = exp.toString() !== 'Invalid Date';
const ttl = isValidDate ? Math.max(MIN_REFRESH_WAIT_MS, exp.getTime() - Date.now()) : DEFAULT_TOKEN_EXP_MS;
return ttl - REFRESH_TIMING_BUFFER_MS;
}
9 changes: 9 additions & 0 deletions superset-embedded-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { IFRAME_COMMS_MESSAGE_TYPE } from './const';

// We can swap this out for the actual switchboard package once it gets published
import { Switchboard } from '@superset-ui/switchboard';
import { getGuestTokenRefreshTiming } from './guestTokenRefresh';

/**
* The function to fetch a guest token from your Host App's backend server.
Expand Down Expand Up @@ -143,6 +144,14 @@ export async function embedDashboard({
ourPort.emit('guestToken', { guestToken });
log('sent guest token');

async function refreshGuestToken() {
const newGuestToken = await fetchGuestToken();
ourPort.emit('guestToken', { guestToken: newGuestToken });
setTimeout(refreshGuestToken, getGuestTokenRefreshTiming(newGuestToken));
}

setTimeout(refreshGuestToken, getGuestTokenRefreshTiming(guestToken));

function unmount() {
log('unmounting');
mountPoint.replaceChildren();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function getInstance(): SupersetClientClass {
const SupersetClient: SupersetClientInterface = {
configure: config => {
singletonClient = new SupersetClientClass(config);
return singletonClient;
return SupersetClient;
},
reset: () => {
singletonClient = undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export interface SupersetClientInterface
| 'isAuthenticated'
| 'reAuthenticate'
> {
configure: (config?: ClientConfig) => SupersetClientClass;
configure: (config?: ClientConfig) => SupersetClientInterface;
reset: () => void;
}

Expand Down
16 changes: 10 additions & 6 deletions superset-frontend/src/embedded/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,12 @@ if (!window.parent) {
// );
// }

async function start(guestToken: string) {
// the preamble configures a client, but we need to configure a new one
// now that we have the guest token
function setupGuestClient(guestToken: string) {
// need to reconfigure SupersetClient to use the guest token
setupClient({
guestToken,
guestTokenHeaderName: bootstrapData.config?.GUEST_TOKEN_HEADER_NAME,
});
ReactDOM.render(<EmbeddedApp />, appMountPoint);
}

function validateMessageEvent(event: MessageEvent) {
Expand All @@ -103,7 +101,7 @@ function validateMessageEvent(event: MessageEvent) {
}
}

window.addEventListener('message', function (event) {
window.addEventListener('message', function embeddedPageInitializer(event) {
try {
validateMessageEvent(event);
} catch (err) {
Expand All @@ -121,8 +119,14 @@ window.addEventListener('message', function (event) {
debug: debugMode,
});

let started = false;

switchboard.defineMethod('guestToken', ({ guestToken }) => {
start(guestToken);
setupGuestClient(guestToken);
if (!started) {
ReactDOM.render(<EmbeddedApp />, appMountPoint);
started = true;
}
});

switchboard.defineMethod('getScrollSize', () => ({
Expand Down
2 changes: 1 addition & 1 deletion superset/security/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1334,7 +1334,7 @@ def create_guest_access_token(
audience = self._get_guest_token_jwt_audience()
# calculate expiration time
now = self._get_current_epoch_time()
exp = now + (exp_seconds * 1000)
exp = now + exp_seconds
claims = {
"user": user,
"resources": resources,
Expand Down
2 changes: 1 addition & 1 deletion tests/integration_tests/security_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1192,7 +1192,7 @@ def test_create_guest_access_token(self, get_time_mock):
self.assertEqual(aud, decoded_token["aud"])
self.assertEqual("guest", decoded_token["type"])
self.assertEqual(
now + (self.app.config["GUEST_TOKEN_JWT_EXP_SECONDS"] * 1000),
now + (self.app.config["GUEST_TOKEN_JWT_EXP_SECONDS"]),
decoded_token["exp"],
)

Expand Down

0 comments on commit 54b60de

Please sign in to comment.