Skip to content
Open
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
3 changes: 3 additions & 0 deletions .env-example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CLIENT_ID=753482910
CLIENT_SECRET=6572195638271537892521
COOKIE_SECRET=325797325
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,32 @@ npm install
```
openssl rsa -in keytmp.pem -out key.pem
```

4. Start the server (must be kept running when using the app in Asana):
4. Create an app in the Asana [developer console](https://app.asana.com/0/my-apps)
5. Naviate to your app's **"App Components"** page `https://app.asana.com/0/my-apps/<YOUR_APP's_ID>/app-components`
6. Configure your **"Modal form"** > **"Form metadata URL"** > `https://localhost:8000/form/metadata`
7. Configure your **"Look up"**:
1. **"Resource attach URL"** > `https://localhost:8000/search/attach`
2. **"Placeholder text"** > `<NAME_THIS_WHATEVER_YOU_WANT>`
3. **"Resource typeahead URL"** > `https://localhost:8000/search/typeahead`
8. Configure your **"Widget"**:
1. **"Widget metadata URL"** > `https://localhost:8000/widget`
2. **"Match URL pattern"** > `^https:\/\/localhost:8000\/(.*)?$` OR if you want to match everything `.*`
9. Congiure your **"Entry point"**:
1. **"Lookup action text"** > `<ENTER_WHATEVER_YOU_WANT>` EX: `Lookup`
2. **"Modal form action text"** > `<ENTER_WHATEVER_YOU_WANT>` EX: `Modal form`
3. **"Dropdown button text"** > `<ENTER_WHATEVER_YOU_WANT>` EX: `Dropdown`
10. Naviate to your app's **"Manage distribution"** page `https://app.asana.com/0/my-apps/<YOUR_APP's_ID>/manage-distribution`
11. Select **"Specific workspaces"** > **"+ Add workspace"** > add a workspace you want your app to be installed on OR select **"Any workspace"** if you want your app to be available to any workspace
12. Naviate to your app's **"OAuth"** page `https://app.asana.com/0/my-apps/<YOUR_APP's_ID>/oauth`
13. Click on **"+ Add redirect URL"** and add the following as your redirect URL `https://localhost:8000/oauth/callback`
14. Under **"Custom authentication URL"** add `https://localhost:8000/oauth` then click on **"Save changes"**
15. Note down your app's `Client ID` and app's `Client secret`. Create a `.env` file in your root directory with the following contents (see `.env-example` for an example of what the contents should look like):
```
CLIENT_ID=<YOUR_APP's_APP_ID/CLIENT_ID>
CLIENT_SECRET=<YOUR_APP's_CLIENT_SECRET>
COOKIE_SECRET=<SOME_SECRET_VALUE>
```
16. Start the server (must be kept running when using the app in Asana):

```
npm run dev
Expand Down
19 changes: 0 additions & 19 deletions auth.html

This file was deleted.

199 changes: 152 additions & 47 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
require("dotenv").config();

// Packages
const axios = require("axios");
const cookieParser = require("cookie-parser");
const cors = require("cors");
const crypto = require("crypto");
const express = require("express");
const https = require("https");
const fs = require("fs");
const cors = require("cors");
const path = require("path");
const app = express();
const https = require("https");
const { v4: uuidv4 } = require("uuid");

// Constants
const port = 8000;
const crypto = require("crypto");
const baseURL = `https://localhost:${port}`;
const redirectUri = `${baseURL}/oauth/callback`;

const app = express();

// Parse JSON bodies
app.use(express.json());
Expand All @@ -17,14 +27,31 @@ app.use(
})
);

// Enable storage of data in cookies.
// Signed cookies are signed by the COOKIE-SECRET environment variable.
app.use(cookieParser(process.env.COOKIE_SECRET));

// Set EJS as the templating engine
app.set('view engine', 'ejs');

// Run before every API request
app.use((req, res, next) => {
// Since Asana does not send `x-asana-request-signature` during oauth exchange.
// Skip `x-asana-request-signature` check if it hits one of our /oauth endpoints (i.e., /oauth and /oauth/callback)
// or if it's a favicon request.
if (req._parsedUrl.pathname.includes('/oauth') || req._parsedUrl.pathname.includes('/favicon.ico')) {
next();
return;
}

// Assess timeliness (https://developers.asana.com/docs/timeliness)
const expirationDate = req.query.expires_at || req.body.expires_at;
const expirationDate = req.query.expires_at || JSON.parse(req.body.data).expires_at;
const currentDate = new Date();

// Check request expiration date if it's included in the request.
if (currentDate.getTime() > new Date(expirationDate).getTime()) {
console.log("Request expired.");
res.status(408).send("Request expired.");
return;
}

Expand All @@ -33,46 +60,124 @@ app.use((req, res, next) => {
// For more information on the Client Secret, feel free to review the link above.

// Verify that the signature exists
// if (!req.headers["x-asana-request-signature"]) {
// console.log("Signature is missing.");
// return;
// }

// let stringToVerify;
// let secret = "my_client_secret_string";

// if (req.method === "POST") {
// stringToVerify = req.body.data.toString();
// } else if (req.method === "GET") {
// stringToVerify = req._parsedUrl.query;
// }

// let computedSignature = crypto
// .createHmac("sha256", secret)
// .update(stringToVerify)
// .digest("hex");
// if (
// !crypto.timingSafeEqual(
// Buffer.from(req.headers["x-asana-request-signature"]),
// Buffer.from(computedSignature)
// )
// ) {
// console.log("Request cannot be verified.");
// res.status(400);
// return;
// } else {
// console.log("Request verified!");
// }
if (!req.headers["x-asana-request-signature"]) {
console.log("Request missing x-asana-request-signature");
res.status(400).send("Request missing x-asana-request-signature");
return;
}

let stringToVerify;

if (req.method === "POST") {
stringToVerify = req.body.data.toString();
} else if (req.method === "GET") {
stringToVerify = req._parsedUrl.query;
}

let computedSignature = crypto
.createHmac("sha256", process.env.CLIENT_SECRET)
.update(stringToVerify)
.digest("hex");

try {
if (crypto.timingSafeEqual(
Buffer.from(req.headers["x-asana-request-signature"]),
Buffer.from(computedSignature)
)) {
console.log("Request verified!");
} else {
console.log("x-asana-request-signature validation failed");
res.status(400).send("x-asana-request-signature validation failed");
return;
}
} catch (error) {
console.log("x-asana-request-signature validation failed");
res.status(400).send("x-asana-request-signature validation failed");
return;
}

next();
});

// -------------------- Client endpoint for auth (see auth.html) --------------------
// -------------------- Client endpoints for OAuth --------------------

// Add this to the `Custom authentication URL` in your app's Asana Developer Console OAuth page
app.get("/oauth", (req, res) => {
// Generate a `state` value and store it. We are generating UUIDs for this example to make it not guessable.
// Docs: https://developers.asana.com/docs/oauth#response
let generatedState = uuidv4();

// Expiration of 5 minutes
res.cookie("state", generatedState, {
maxAge: 1000 * 60 * 5,
signed: true,
});

let userAuthorizationLink = `https://app.asana.com/-/oauth_authorize?response_type=code&client_id=${process.env.CLIENT_ID}&redirect_uri=${redirectUri}&state=${generatedState}`

res.render("app_server_app_auth_page", {"userAuthorizationLink": userAuthorizationLink});
});

// Add this to the `Redirect URLs` in your app's Asana Developer Console OAuth page
app.get("/oauth/callback", (req, res) => {
// Prevent CSRF attacks by validating the 'state' parameter.
// Docs: https://developers.asana.com/docs/oauth#user-authorization-endpoint
if (req.query.state !== req.signedCookies.state) {
res.status(422).send("The 'state' parameter does not match.");
return;
}

// Check if the user clicked on "deny" on the grant permissions page.
// If so, let Asana know that the app auth failed.
if(req.query.error === 'access_denied') {
res.render("send_app_auth_info", {"status": 'error'});
}

console.log(
"***** Code (to be exchanged for a token) and state from the user authorization response:\n"
);

// Body of the POST request to the token exchange endpoint.
const body = {
grant_type: "authorization_code",
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
redirect_uri: redirectUri,
code: req.query.code,
};

// Set Axios to serialize the body to urlencoded format.
const config = {
headers: {
"content-type": "application/x-www-form-urlencoded",
},
};

// Make the request to the token exchange endpoint.
// Docs: https://developers.asana.com/docs/oauth#token-exchange-endpoint
axios
.post("https://app.asana.com/-/oauth_token", body, config)
.then((res) => {
console.log("***** Response from the token exchange request:\n");
console.log(res.data);
return res.data;
})
.then((data) => {
// Store tokens in cookies.
// In a production app, you should store this data somewhere secure and durable instead (e.g., a database).
res.cookie("access_token", data.access_token, { maxAge: 60 * 60 * 1000 });
res.cookie("refresh_token", data.refresh_token, {
// Prevent client-side scripts from accessing this data.
httpOnly: true,
secure: true,
});

app.get("/auth", (req, res) => {
// We recommend creating a secure Oauth flow (https://developers.asana.com/docs/oauth)
console.log("Auth happened!");
res.sendFile(path.join(__dirname, "/auth.html"));
// Let Asana know that the app component auth has completed successfully
res.render("send_app_auth_info", {"status": 'success'});
})
.catch((err) => {
console.log(err.message);
});
});

// -------------------- API endpoints --------------------
Expand Down Expand Up @@ -121,7 +226,7 @@ app.post("/form/submit", (req, res) => {

attachment_response = {
resource_name: "I'm an Attachment",
resource_url: "https://localhost:8000",
resource_url: baseURL,
};

// Docs: https://developers.asana.com/docs/widget
Expand Down Expand Up @@ -176,7 +281,7 @@ form_response = {
template: "form_metadata_v0",
metadata: {
title: "I'm a title",
on_submit_callback: "https://localhost:8000/form/submit",
on_submit_callback: `${baseURL}/form/submit`,
fields: [
{
name: "I'm a single_line_text",
Expand Down Expand Up @@ -310,7 +415,7 @@ form_response = {
type: "typeahead",
id: "typeahead_half_width",
is_required: false,
typeahead_url: "https://localhost:8000/search/typeahead",
typeahead_url: `${baseURL}/search/typeahead`,
placeholder: "[half width]",
width: "half",
},
Expand All @@ -319,12 +424,12 @@ form_response = {
type: "typeahead",
id: "typeahead_full_width",
is_required: false,
typeahead_url: "https://localhost:8000/search/typeahead",
typeahead_url: `${baseURL}/search/typeahead`,
placeholder: "[full width]",
width: "full",
},
],
on_change_callback: "https://localhost:8000/form/onchange",
on_change_callback: `${baseURL}/form/onchange`,
},
};

Expand Down Expand Up @@ -355,6 +460,6 @@ https
)
.listen(port, function () {
console.log(
`Example app listening on port ${port}! Go to https://localhost:${port}/`
`Example app listening on port ${port}! Base URL: ${baseURL}`
);
});
Loading