Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ app.post("/authorize", async (c) => {
// redirect URL as query params = new URL("/callback", c.req.url).href to
// send the user back to callback endpoint.
// The callback endpoint will get the encrypted token and decrypt it to get the user's access token.

// const targetURLAuthorize = new URL("callosum/v1/v2/auth/token/authorize", instanceUrl);
// targetURLAuthorize.searchParams.append('validity_time_in_sec', "86400");
// const targetURLCallbackPath = new URL("/callback", c.req.url);
// targetURLCallbackPath.searchParams.append('instanceUrl', instanceUrl);
// targetURLAuthorize.searchParams.append('redirect_url', btoa(targetURLCallbackPath.toString()));
// const encodedState = btoa(JSON.stringify(state.oauthReqInfo));
// targetURLAuthorize.searchParams.append('state', encodedState);
// targetURLAuthorize.searchParams.append('token_encryption_key', "1234567812345678");
// targetURLAuthorize.searchParams.append('encryption_algorithm', 'AES');
// redirectUrl.searchParams.append('targetURLPath', targetURLAuthorize.href);

const targetURLPath = new URL("/callback", c.req.url);
targetURLPath.searchParams.append('instanceUrl', instanceUrl);
const encodedState = btoa(JSON.stringify(state.oauthReqInfo));
Expand All @@ -64,6 +76,13 @@ app.post("/authorize", async (c) => {
})

app.get("/callback", async (c) => {

// TODO(shikhar.bhargava): remove this once we have a proper callback URL
// With the proper callback URL, we will get the encrypted token in the query params
// along with it we will get the instanceUrl and the state (oauthReqInfo).
// and we will decrypt the token to get the user's access token and complete the authorization.
// const encodedOauthReqInfo = c.req.query('state');

const instanceUrl = c.req.query('instanceUrl');
const encodedOauthReqInfo = c.req.query('oauthReqInfo');
if (!instanceUrl) {
Expand Down
39 changes: 36 additions & 3 deletions src/oauth-manager/oauth-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ export function renderApprovalDialog(request: Request, options: ApprovalDialogOp

<div class="form-group">
<label for="instanceUrl">ThoughtSpot Instance URL</label>
<input type="url" id="instanceUrl" name="instanceUrl" required
<input type="text" id="instanceUrl" name="instanceUrl" required
placeholder="https://your-instance.thoughtspot.cloud">
</div>

Expand Down Expand Up @@ -498,6 +498,36 @@ export interface ParsedApprovalResult {
}


/**
* Validates and sanitizes a URL to ensure it's a valid ThoughtSpot instance URL
* @param url - The URL to validate and sanitize
* @returns The sanitized URL
* @throws Error if the URL is invalid
*/
function validateAndSanitizeUrl(url: string): string {
try {
// Remove any whitespace
const trimmedUrl = url.trim();

// Add https:// if no protocol is specified
const urlWithProtocol = trimmedUrl.startsWith('http://') || trimmedUrl.startsWith('https://')
? trimmedUrl
: `https://${trimmedUrl}`;

const parsedUrl = new URL(urlWithProtocol);

// Remove trailing slashes and normalize the URL
const sanitizedUrl = parsedUrl.origin;

return sanitizedUrl;
} catch (e) {
if (e instanceof Error) {
throw new Error(`Invalid URL: ${e.message}`);
}
throw new Error('Invalid URL format');
}
}

/**
* Parses the form submission from the approval dialog, extracts the state,
* and generates Set-Cookie headers to mark the client as approved.
Expand All @@ -517,7 +547,7 @@ export async function parseRedirectApproval(request: Request): Promise<ParsedApp
try {
const formData = await request.formData()
const encodedState = formData.get('state')
instanceUrl = formData.get('instanceUrl') as string;
const rawInstanceUrl = formData.get('instanceUrl') as string;

if (typeof encodedState !== 'string' || !encodedState) {
throw new Error("Missing or invalid 'state' in form data.")
Expand All @@ -530,9 +560,12 @@ export async function parseRedirectApproval(request: Request): Promise<ParsedApp
throw new Error('Could not extract clientId from state object.')
}

if (!instanceUrl) {
if (!rawInstanceUrl) {
throw new Error('Missing instance URL')
}

// Validate and sanitize the instance URL
instanceUrl = validateAndSanitizeUrl(rawInstanceUrl);
} catch (e) {
console.error('Error processing form submission:', e)
throw new Error(`Failed to parse approval form: ${e instanceof Error ? e.message : String(e)}`)
Expand Down