-
Notifications
You must be signed in to change notification settings - Fork 110
Feat/clean dropbox oauth #737
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
18ffa9a
8b0add8
449120c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,4 +63,9 @@ typings/ | |
| .DS_Store | ||
|
|
||
| # ActionHub specifics | ||
| status.json | ||
| status.json | ||
|
|
||
| # Internal Tooling | ||
| .agent/ | ||
| .gemini/ | ||
| tasks/ | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -52,11 +52,25 @@ class AirtableAction extends Hub.OAuthAction { | |||||||||||||||||||||||||||||||
| let accessToken; | ||||||||||||||||||||||||||||||||
| let tokens; | ||||||||||||||||||||||||||||||||
| if (request.params.state_json) { | ||||||||||||||||||||||||||||||||
| const stateJson = await this.oauthExtractTokensFromStateJson(request.params.state_json, request.webhookId); | ||||||||||||||||||||||||||||||||
| tokens = airtable_tokens_1.AirtableTokens.fromJson(stateJson); | ||||||||||||||||||||||||||||||||
| accessToken = tokens.access_token; | ||||||||||||||||||||||||||||||||
| const encrypted = await this.oauthMaybeEncryptTokens(new airtable_tokens_1.AirtableTokens(tokens.refresh_token, accessToken, tokens.redirectUri), request.webhookId); | ||||||||||||||||||||||||||||||||
| state.data = typeof encrypted === "string" ? encrypted : JSON.stringify(encrypted); | ||||||||||||||||||||||||||||||||
| const parsedState = JSON.parse(request.params.state_json); | ||||||||||||||||||||||||||||||||
| if (parsedState.cid && parsedState.payload) { | ||||||||||||||||||||||||||||||||
| const stateJson = await this.oauthExtractTokensFromStateJson(request.params.state_json, request.webhookId); | ||||||||||||||||||||||||||||||||
| tokens = airtable_tokens_1.AirtableTokens.fromJson(stateJson); | ||||||||||||||||||||||||||||||||
| accessToken = tokens.access_token; | ||||||||||||||||||||||||||||||||
| const encrypted = await this.oauthMaybeEncryptTokens(new airtable_tokens_1.AirtableTokens(tokens.refresh_token, accessToken, tokens.redirectUri), request.webhookId); | ||||||||||||||||||||||||||||||||
| state.data = typeof encrypted === "string" ? encrypted : JSON.stringify(encrypted); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| else { | ||||||||||||||||||||||||||||||||
| // Keeping the literal old code to ensure no regressions for unencrypted payloads | ||||||||||||||||||||||||||||||||
| tokens = airtable_tokens_1.AirtableTokens.fromJson(parsedState); | ||||||||||||||||||||||||||||||||
| accessToken = tokens.access_token; | ||||||||||||||||||||||||||||||||
| state.data = JSON.stringify({ | ||||||||||||||||||||||||||||||||
| tokens: { | ||||||||||||||||||||||||||||||||
| refresh_token: tokens.refresh_token, | ||||||||||||||||||||||||||||||||
| access_token: accessToken, | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| if (!accessToken) { | ||||||||||||||||||||||||||||||||
|
|
@@ -104,14 +118,30 @@ class AirtableAction extends Hub.OAuthAction { | |||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| async form(request) { | ||||||||||||||||||||||||||||||||
| // The form function handles the Airtable configuration form. | ||||||||||||||||||||||||||||||||
| // It attempts to list the user's bases using the existing tokens in request.params.state_json. | ||||||||||||||||||||||||||||||||
| // If listing succeeds, we return the base/table fields directly. | ||||||||||||||||||||||||||||||||
| // if listing fails (e.g., token expired), we try to auto-refresh the token. | ||||||||||||||||||||||||||||||||
| // If no tokens exist or refresh fails, it catches the error (in the outer catch block) | ||||||||||||||||||||||||||||||||
| // and generates an OAuth link with a PKCE code_verifier to allow the user to re-authenticate. | ||||||||||||||||||||||||||||||||
| const form = new Hub.ActionForm(); | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| let accessToken; | ||||||||||||||||||||||||||||||||
| let tokens; | ||||||||||||||||||||||||||||||||
| let isEncrypted = false; | ||||||||||||||||||||||||||||||||
| if (request.params.state_json) { | ||||||||||||||||||||||||||||||||
| const stateJson = await this.oauthExtractTokensFromStateJson(request.params.state_json, request.webhookId); | ||||||||||||||||||||||||||||||||
| tokens = airtable_tokens_1.AirtableTokens.fromJson(stateJson); | ||||||||||||||||||||||||||||||||
| accessToken = tokens.access_token; | ||||||||||||||||||||||||||||||||
| const parsedState = JSON.parse(request.params.state_json); | ||||||||||||||||||||||||||||||||
| if (parsedState.cid && parsedState.payload) { | ||||||||||||||||||||||||||||||||
| isEncrypted = true; | ||||||||||||||||||||||||||||||||
| const stateJson = await this.oauthExtractTokensFromStateJson(request.params.state_json, request.webhookId); | ||||||||||||||||||||||||||||||||
| tokens = airtable_tokens_1.AirtableTokens.fromJson(stateJson); | ||||||||||||||||||||||||||||||||
| accessToken = tokens.access_token; | ||||||||||||||||||||||||||||||||
|
Comment on lines
+136
to
+138
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the issue in the
Suggested change
|
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| else { | ||||||||||||||||||||||||||||||||
| // Keeping the literal old code to ensure no regressions for unencrypted payloads | ||||||||||||||||||||||||||||||||
| tokens = airtable_tokens_1.AirtableTokens.fromJson(parsedState); | ||||||||||||||||||||||||||||||||
| accessToken = tokens.access_token; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| if (!accessToken) { | ||||||||||||||||||||||||||||||||
|
|
@@ -120,12 +150,16 @@ class AirtableAction extends Hub.OAuthAction { | |||||||||||||||||||||||||||||||
| await this.checkBaseList(accessToken); | ||||||||||||||||||||||||||||||||
| if (form.state === undefined) { | ||||||||||||||||||||||||||||||||
| form.state = new hub_1.ActionState(); | ||||||||||||||||||||||||||||||||
| if (tokens) { | ||||||||||||||||||||||||||||||||
| if (isEncrypted && tokens) { | ||||||||||||||||||||||||||||||||
| const encrypted = await this.oauthMaybeEncryptTokens(tokens, request.webhookId); | ||||||||||||||||||||||||||||||||
| const encryptedStr = typeof encrypted === "string" ? encrypted : JSON.stringify(encrypted); | ||||||||||||||||||||||||||||||||
| request.params.state_json = encryptedStr; | ||||||||||||||||||||||||||||||||
| form.state.data = encryptedStr; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| else { | ||||||||||||||||||||||||||||||||
| // Keeping the literal old code to ensure no regressions for unencrypted payloads | ||||||||||||||||||||||||||||||||
| form.state.data = request.params.state_json; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| catch (_a) { | ||||||||||||||||||||||||||||||||
|
|
@@ -160,7 +194,8 @@ class AirtableAction extends Hub.OAuthAction { | |||||||||||||||||||||||||||||||
| }]; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| catch (e) { | ||||||||||||||||||||||||||||||||
| // prevents others from impersonating you | ||||||||||||||||||||||||||||||||
| // If no valid tokens exist (or refresh fail), we generate an OAuth link fallback. | ||||||||||||||||||||||||||||||||
| // We create a code_verifier (PKCE) and encrypt it in the state payload to secure the exchange. | ||||||||||||||||||||||||||||||||
| const codeVerifier = crypto.randomBytes(96).toString("base64url"); // 128 characters | ||||||||||||||||||||||||||||||||
| const actionCrypto = new Hub.ActionCrypto(); | ||||||||||||||||||||||||||||||||
| const jsonString = JSON.stringify({ stateurl: request.params.state_url, verifier: codeVerifier }); | ||||||||||||||||||||||||||||||||
|
|
@@ -177,16 +212,26 @@ class AirtableAction extends Hub.OAuthAction { | |||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return form; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| // oauthCheck determines if the user is authenticated by verifying that request.params.state_json | ||||||||||||||||||||||||||||||||
| // contains valid (encrypted or unencrypted) token state. | ||||||||||||||||||||||||||||||||
| async oauthCheck(request) { | ||||||||||||||||||||||||||||||||
| if (request.params.state_json) { | ||||||||||||||||||||||||||||||||
| const stateJson = await this.oauthExtractTokensFromStateJson(request.params.state_json, request.webhookId); | ||||||||||||||||||||||||||||||||
| if (stateJson) { | ||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||
| const parsedState = JSON.parse(request.params.state_json); | ||||||||||||||||||||||||||||||||
| if (parsedState.cid && parsedState.payload) { | ||||||||||||||||||||||||||||||||
| const stateJson = await this.oauthExtractTokensFromStateJson(request.params.state_json, request.webhookId); | ||||||||||||||||||||||||||||||||
| return !!stateJson; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| else { | ||||||||||||||||||||||||||||||||
| // Keeping the literal old code to ensure no regressions for unencrypted payloads | ||||||||||||||||||||||||||||||||
| const tokens = airtable_tokens_1.AirtableTokens.fromJson(parsedState); | ||||||||||||||||||||||||||||||||
| return !!tokens.access_token; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+219
to
228
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| async oauthFetchInfo(urlParams, redirectUri) { | ||||||||||||||||||||||||||||||||
| // oauthFetchInfo exchanges the authorization code for access and refresh tokens from Airtable. | ||||||||||||||||||||||||||||||||
| // We decrypt the state to retrieve the stateurl and code_verifier. | ||||||||||||||||||||||||||||||||
| const actionCrypto = new Hub.ActionCrypto(); | ||||||||||||||||||||||||||||||||
| const plaintext = await actionCrypto.decrypt(urlParams.state).catch((err) => { | ||||||||||||||||||||||||||||||||
| winston.error("Encryption not correctly configured" + err); | ||||||||||||||||||||||||||||||||
|
|
@@ -214,12 +259,14 @@ class AirtableAction extends Hub.OAuthAction { | |||||||||||||||||||||||||||||||
| // Pass back context to Looker | ||||||||||||||||||||||||||||||||
| if (response.status === 200) { | ||||||||||||||||||||||||||||||||
| const data = response.data; | ||||||||||||||||||||||||||||||||
| // Successful token exchange. Return tokens to Looker via the stateurl callback. | ||||||||||||||||||||||||||||||||
| const tokenPayload = new airtable_tokens_1.AirtableTokens(data.refresh_token, data.access_token, redirectUri); | ||||||||||||||||||||||||||||||||
| const encrypted = await this.oauthMaybeEncryptTokens(tokenPayload, undefined); | ||||||||||||||||||||||||||||||||
| // In this case we expect this function to return an encrypted token because AirTable is "enabled" for encryption. | ||||||||||||||||||||||||||||||||
| const payloadWithEncryptedToken = await this.oauthMaybeEncryptTokens(tokenPayload, undefined); | ||||||||||||||||||||||||||||||||
| await gaxios.request({ | ||||||||||||||||||||||||||||||||
| url: payload.stateurl, | ||||||||||||||||||||||||||||||||
| method: "POST", | ||||||||||||||||||||||||||||||||
| body: encrypted, | ||||||||||||||||||||||||||||||||
| data: payloadWithEncryptedToken, | ||||||||||||||||||||||||||||||||
| }).catch((_err) => { winston.error(_err.toString()); }); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| else { | ||||||||||||||||||||||||||||||||
|
|
@@ -228,6 +275,7 @@ class AirtableAction extends Hub.OAuthAction { | |||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| async oauthUrl(redirectUri, encryptedState) { | ||||||||||||||||||||||||||||||||
| // oauthUrl constructs the authorization URL to redirect the user to Airtable's auth page. | ||||||||||||||||||||||||||||||||
| const clientId = process.env.AIRTABLE_CLIENT_ID ? process.env.AIRTABLE_CLIENT_ID : "must exist"; | ||||||||||||||||||||||||||||||||
| const actionCrypto = new Hub.ActionCrypto(); | ||||||||||||||||||||||||||||||||
| const plaintext = await actionCrypto.decrypt(encryptedState).catch((err) => { | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The call to
AirtableTokens.fromJson(stateJson)is unsafe becauseoauthExtractTokensFromStateJsonreturnsnullif decryption fails or the JSON is malformed. This will cause a crash when attempting to access properties onnull. You should ensurestateJsonis truthy before proceeding.