From c457ecbd20aef5baac64d8277860b4438c17a177 Mon Sep 17 00:00:00 2001 From: Brian Graves Date: Wed, 5 Nov 2025 16:05:32 -0800 Subject: [PATCH 1/2] Fix /oauth_token route to correctly handle POST (#698) Contains fixes to V2 OAuth flow: * Fix /oauth_token route to correctly handle POST body * Adds fetchTokenState to Hub.ActionRequest to store posted state data if present. * Modify GoogleDriveAction to properly handle above changes * provide asJson method for ActionToken type --- lib/actions/google/drive/google_drive.js | 6 +++--- lib/api_types/data_webhook_payload.d.ts | 2 ++ lib/hub/action_request.d.ts | 1 + lib/hub/action_request.js | 3 +++ lib/hub/action_token.d.ts | 1 + lib/hub/action_token.js | 6 ++++++ lib/server/server.js | 2 +- src/actions/google/drive/google_drive.ts | 6 +++--- src/actions/google/drive/test_google_drive.ts | 3 +-- src/api_types/data_webhook_payload.ts | 2 ++ src/hub/action_request.ts | 5 +++++ src/hub/action_token.ts | 7 +++++++ src/server/server.ts | 2 +- test/test_action_request.ts | 19 +++++++++++++++++++ 14 files changed, 55 insertions(+), 10 deletions(-) diff --git a/lib/actions/google/drive/google_drive.js b/lib/actions/google/drive/google_drive.js index 9234eba41..7b26db694 100644 --- a/lib/actions/google/drive/google_drive.js +++ b/lib/actions/google/drive/google_drive.js @@ -238,10 +238,10 @@ class GoogleDriveAction extends Hub.OAuthActionV2 { } } async oauthFetchAccessToken(request) { - if (request.params.state) { + if (request.fetchTokenState) { const actionCrypto = new Hub.ActionCrypto(); - const plaintext = await actionCrypto.decrypt(request.params.state).catch((err) => { - winston.error("Encryption not correctly configured" + err); + const plaintext = await actionCrypto.decrypt(request.fetchTokenState).catch((err) => { + winston.error("Encryption not correctly configured", { error: err }); throw err; }); const state = JSON.parse(plaintext); diff --git a/lib/api_types/data_webhook_payload.d.ts b/lib/api_types/data_webhook_payload.d.ts index 0fe06340e..fee92cae1 100644 --- a/lib/api_types/data_webhook_payload.d.ts +++ b/lib/api_types/data_webhook_payload.d.ts @@ -20,6 +20,8 @@ export interface DataWebhookPayload { form_params: { [key: string]: string; } | null; + /** Encrypted data used by /actions/:actionId/oauth_token route */ + state: string | null; } export interface RequestDataWebhookPayload { } diff --git a/lib/hub/action_request.d.ts b/lib/hub/action_request.d.ts index 24c80db99..05064c948 100644 --- a/lib/hub/action_request.d.ts +++ b/lib/hub/action_request.d.ts @@ -45,6 +45,7 @@ export declare class ActionRequest { formParams: ParamMap; params: ParamMap; scheduledPlan?: ActionScheduledPlan; + fetchTokenState?: string; type: ActionType; actionId?: string; instanceId?: string; diff --git a/lib/hub/action_request.js b/lib/hub/action_request.js index 1bf99e37e..c72102396 100644 --- a/lib/hub/action_request.js +++ b/lib/hub/action_request.js @@ -85,6 +85,9 @@ class ActionRequest { if (json.form_params) { request.formParams = json.form_params; } + if (json.state) { + request.fetchTokenState = json.state; + } return request; } empty() { diff --git a/lib/hub/action_token.d.ts b/lib/hub/action_token.d.ts index 39594996f..932788a3c 100644 --- a/lib/hub/action_token.d.ts +++ b/lib/hub/action_token.d.ts @@ -2,4 +2,5 @@ export declare class ActionToken { tokens: any; redirect: any; constructor(tokens: any, redirect: any); + asJson(): any; } diff --git a/lib/hub/action_token.js b/lib/hub/action_token.js index e7bafda62..611096061 100644 --- a/lib/hub/action_token.js +++ b/lib/hub/action_token.js @@ -6,5 +6,11 @@ class ActionToken { this.tokens = tokens; this.redirect = redirect; } + asJson() { + return { + tokens: this.tokens, + redirect: this.redirect, + }; + } } exports.ActionToken = ActionToken; diff --git a/lib/server/server.js b/lib/server/server.js index 1f55eb714..ac06de00d 100644 --- a/lib/server/server.js +++ b/lib/server/server.js @@ -186,7 +186,7 @@ class Server { if ((0, hub_1.isOauthActionV2)(action)) { const payload = await action.oauthFetchAccessToken(request); res.type("json"); - res.send(JSON.stringify(payload)); + res.send(JSON.stringify(payload.asJson())); } else { res.statusCode = 404; diff --git a/src/actions/google/drive/google_drive.ts b/src/actions/google/drive/google_drive.ts index 995c09d2e..34828fdcc 100644 --- a/src/actions/google/drive/google_drive.ts +++ b/src/actions/google/drive/google_drive.ts @@ -283,10 +283,10 @@ export class GoogleDriveAction extends Hub.OAuthActionV2 { } async oauthFetchAccessToken(request: Hub.ActionRequest) { - if (request.params.state) { + if (request.fetchTokenState) { const actionCrypto = new Hub.ActionCrypto() - const plaintext = await actionCrypto.decrypt(request.params.state).catch((err: string) => { - winston.error("Encryption not correctly configured" + err) + const plaintext = await actionCrypto.decrypt(request.fetchTokenState).catch((err: string) => { + winston.error("Encryption not correctly configured", { error: err }) throw err }) const state = JSON.parse(plaintext) diff --git a/src/actions/google/drive/test_google_drive.ts b/src/actions/google/drive/test_google_drive.ts index 1f240c221..c4e1e6ea3 100644 --- a/src/actions/google/drive/test_google_drive.ts +++ b/src/actions/google/drive/test_google_drive.ts @@ -405,8 +405,7 @@ describe(`${action.constructor.name} unit tests`, () => { const stubAccessToken = sinon.stub(action as any, "getAccessTokenCredentialsFromCode") .resolves({tokens: "token"}) const request = new Hub.ActionRequest() - request.params = { - state: "eyJjb2RlIjoiY29kZSIsInJlZGlyZWN0dXJpIjoicmVkaXJlY3QifQ"} + request.fetchTokenState = "eyJjb2RlIjoiY29kZSIsInJlZGlyZWN0dXJpIjoicmVkaXJlY3QifQ" request.webhookId = "testId" const tokenPayload = action.oauthFetchAccessToken(request) chai.expect(tokenPayload).to.eventually.deep.equal({tokens: {tokens: "token"}, redirect: "redirect"}) diff --git a/src/api_types/data_webhook_payload.ts b/src/api_types/data_webhook_payload.ts index 03684f6d8..066052d70 100644 --- a/src/api_types/data_webhook_payload.ts +++ b/src/api_types/data_webhook_payload.ts @@ -20,6 +20,8 @@ export interface DataWebhookPayload { data: {[key: string]: string} | null /** Form parameters associated with the payload. */ form_params: {[key: string]: string} | null + /** Encrypted data used by /actions/:actionId/oauth_token route */ + state: string | null } export interface RequestDataWebhookPayload { diff --git a/src/hub/action_request.ts b/src/hub/action_request.ts index 079494860..b9fac28f1 100644 --- a/src/hub/action_request.ts +++ b/src/hub/action_request.ts @@ -140,6 +140,10 @@ export class ActionRequest { request.formParams = json.form_params } + if (json.state) { + request.fetchTokenState = json.state + } + return request } @@ -147,6 +151,7 @@ export class ActionRequest { formParams: ParamMap = {} params: ParamMap = {} scheduledPlan?: ActionScheduledPlan + fetchTokenState?: string type!: ActionType actionId?: string instanceId?: string diff --git a/src/hub/action_token.ts b/src/hub/action_token.ts index 3feac524c..b8ef0a751 100644 --- a/src/hub/action_token.ts +++ b/src/hub/action_token.ts @@ -1,3 +1,10 @@ export class ActionToken { constructor(public tokens: any, public redirect: any) {} + + asJson(): any { + return { + tokens: this.tokens, + redirect: this.redirect, + } + } } diff --git a/src/server/server.ts b/src/server/server.ts index c9e9fd3f0..11299010c 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -204,7 +204,7 @@ export default class Server implements Hub.RouteBuilder { if (isOauthActionV2(action)) { const payload = await (action as OAuthActionV2).oauthFetchAccessToken(request) res.type("json") - res.send(JSON.stringify(payload)) + res.send(JSON.stringify(payload.asJson())) } else { res.statusCode = 404 } diff --git a/test/test_action_request.ts b/test/test_action_request.ts index 1d31ffe3b..c0be88af1 100644 --- a/test/test_action_request.ts +++ b/test/test_action_request.ts @@ -5,6 +5,25 @@ import {mockReq} from "sinon-express-mock" import { ActionRequest } from "../src/hub" describe("ActionRequest", () => { + it("fromRequest correctly parses state from body", () => { + const req = mockReq({ + headers: { + "user-agent": "LookerOutgoingWebhook/7.3.0", + "x-looker-webhook-id": "123", + "x-looker-instance": "instanceId1", + }, + body: { + state: "someEncryptedStateString", + }, + }) + + // @ts-ignore + req.header = (name: string): string | string[] | undefined => req.headers[name] + + const result = ActionRequest.fromRequest(req) + chai.expect(result.fetchTokenState).to.equal("someEncryptedStateString") + }) + it("fromRequest", () => { const req = mockReq({ From db5d30cc3d40d5ba40a65301e29b9324f4fb7931 Mon Sep 17 00:00:00 2001 From: Brian Graves Date: Wed, 5 Nov 2025 16:20:07 -0800 Subject: [PATCH 2/2] Version bump to 1.5.27 (#699) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 880695520..a42e716c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "looker-action-hub", - "version": "1.5.26", + "version": "1.5.27", "description": "", "main": "lib/index", "scripts": {