Skip to content

Commit bf3af40

Browse files
authored
Merge pull request #47 from Asana/aw-asana-node-oauth-demo
Add OAuth Demo in JavaScript.
2 parents bd3c393 + cc32dd2 commit bf3af40

File tree

12 files changed

+1446
-0
lines changed

12 files changed

+1446
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CLIENT_ID=753482910
2+
CLIENT_SECRET=6572195638271537892521
3+
REDIRECT_URI=http://localhost:3000/oauth-callback
4+
COOKIE_SECRET=325797325
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
.env
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# OAuth Demo
2+
3+
The OAuth Demo is an application that demonstrates authorization with a user's Asana account via a basic OAuth server (built with [Express](https://expressjs.com/)). The application shows how you might send a user through the [user authorization endpoint](https://developers.asana.com/docs/oauth#user-authorization-endpoint), as well as how a `code` can be exchanged for a token via the [token exchange endpoint](https://developers.asana.com/docs/oauth#token-exchange-endpoint).
4+
5+
_Note: This OAuth server should only be used for testing and learning purposes._
6+
7+
Documentation: Asana's [OAuth](https://developers.asana.com/docs/oauth)
8+
9+
## Requirements
10+
11+
The application was built with Node v19.4.0.
12+
13+
Visit [Node.js](https://nodejs.org/en/download/) get the latest version for your local machine.
14+
15+
## Installation
16+
17+
After cloning this project, navigate to the root directory and install dependencies:
18+
19+
```
20+
npm i
21+
```
22+
23+
## Usage
24+
25+
1. [Create an application](https://developers.asana.com/docs/oauth#register-an-application). Take note of your:
26+
27+
* Client ID
28+
* Client secret
29+
* Redirect URI
30+
31+
2. Create a `./env` file (in the root directory of the project) with the required configurations:
32+
33+
```
34+
CLIENT_ID=your_client_id_here
35+
CLIENT_SECRET=your_client_secret_here
36+
REDIRECT_URI=your_redirect_uri_here
37+
COOKIE_SECRET=can_be_any_value
38+
```
39+
40+
You can view an example in the included `./env-example` file. Note that you should _never_ commit or otherwise expose your `./env` file publicly.
41+
42+
3. Start the server:
43+
44+
```
45+
npm run dev
46+
```
47+
48+
4. Visit [http://localhost:3000](http://localhost:3000) and click on "Authenticate with Asana"
49+
50+
![user auth screen](./images/mainscreen.png)
51+
52+
5. Select "Allow" to grant the application access to your Asana account
53+
54+
![user auth screen](./images/userauth.png)
55+
56+
You may also wish to view helpful outputs and notes in your terminal as well.
57+
58+
6. After successful authentication, you will be notified and redirected by the application
59+
60+
![user auth screen](./images/authedscreen.png)
61+
62+
Your access token (with an expiration of one hour) will also be loaded into the URL as a query parameter. With the access token, you can:
63+
64+
* Select "Fetch your user info!" to have the application make a request to [GET /users/me](https://developers.asana.com/reference/getuser) on your behalf (and output the response as JSON in the browser)
65+
* Use the access token to make an API request yourself (e.g., via the [API Explorer](https://developers.asana.com/docs/api-explorer), [Postman Collection](https://developers.asana.com/docs/postman-collection), etc.)
66+
67+
## Deauthorizing the demo app
68+
69+
To remove the app from your list of Authorized Apps:
70+
71+
1. Click on your profile photo in the top right corner of the [Asana app](https://app.asana.com)
72+
2. Select "My Settings"
73+
3. Select the "App" tab
74+
4. Select "Deauthorize" next to your application's name
75+
76+
Once deauthorized, you must begin the OAuth process again to authenticate with Asana.
115 KB
Loading
116 KB
Loading
251 KB
Loading
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
require("dotenv").config();
2+
const axios = require("axios");
3+
const express = require("express");
4+
const path = require("path");
5+
const cors = require("cors");
6+
const cookieParser = require("cookie-parser");
7+
const { v4: uuidv4 } = require("uuid");
8+
9+
const app = express();
10+
11+
// Enable CORS and assume the allowed origin is the redirect URI.
12+
// i.e., this assumes that your client shares the same domain as the server.
13+
app.use(
14+
cors({
15+
origin: "http://localhost:3000",
16+
})
17+
);
18+
19+
// Enable storage of data in cookies.
20+
// Signed cookies are signed by the COOKIE-SECRET environment variable.
21+
app.use(cookieParser(process.env.COOKIE_SECRET));
22+
23+
// Serve files in the ./static folder.
24+
app.use(express.static("static"));
25+
26+
// Send static index.html page to the client.
27+
// This page includes a button to authenticatate with Asana.
28+
app.get("/", (req, res) => {
29+
res.sendFile(path.join(__dirname, "/static/index.html"));
30+
});
31+
32+
// When the user clicks on the "Authenticate with Asana" button (from index.html),
33+
// it redirects them to the user authorization endpoint.
34+
// Docs: https://developers.asana.com/docs/oauth#user-authorization-endpoint
35+
app.get("/authenticate", (req, res) => {
36+
// Generate a `state` value and store it
37+
// Docs: https://developers.asana.com/docs/oauth#response
38+
let generatedState = uuidv4();
39+
40+
// Expiration of 5 minutes
41+
res.cookie("state", generatedState, {
42+
maxAge: 1000 * 60 * 5,
43+
signed: true,
44+
});
45+
46+
res.redirect(
47+
`https://app.asana.com/-/oauth_authorize?response_type=code&client_id=${process.env.CLIENT_ID}&redirect_uri=${process.env.REDIRECT_URI}&state=${generatedState}`
48+
);
49+
});
50+
51+
// Redirect the user here upon successful or failed authentications.
52+
// This endpoint on your server must be accessible via the redirect URL that you provided in the developer console.
53+
// Docs: https://developers.asana.com/docs/oauth#register-an-application
54+
app.get("/oauth-callback", (req, res) => {
55+
// Prevent CSRF attacks by validating the 'state' parameter.
56+
// Docs: https://developers.asana.com/docs/oauth#user-authorization-endpoint
57+
if (req.query.state !== req.signedCookies.state) {
58+
res.status(422).send("The 'state' parameter does not match.");
59+
return;
60+
}
61+
62+
console.log(
63+
"***** Code (to be exchanged for a token) and state from the user authorization response:\n"
64+
);
65+
console.log(`code: ${req.query.code}`);
66+
console.log(`state: ${req.query.state}\n`);
67+
68+
// Body of the POST request to the token exchange endpoint.
69+
const body = {
70+
grant_type: "authorization_code",
71+
client_id: process.env.CLIENT_ID,
72+
client_secret: process.env.CLIENT_SECRET,
73+
redirect_uri: process.env.REDIRECT_URI,
74+
code: req.query.code,
75+
};
76+
77+
// Set Axios to serialize the body to urlencoded format.
78+
const config = {
79+
headers: {
80+
"content-type": "application/x-www-form-urlencoded",
81+
},
82+
};
83+
84+
// Make the request to the token exchange endpoint.
85+
// Docs: https://developers.asana.com/docs/oauth#token-exchange-endpoint
86+
axios
87+
.post("https://app.asana.com/-/oauth_token", body, config)
88+
.then((res) => {
89+
console.log("***** Response from the token exchange request:\n");
90+
console.log(res.data);
91+
return res.data;
92+
})
93+
.then((data) => {
94+
// Store tokens in cookies.
95+
// In a production app, you should store this data somewhere secure and durable instead (e.g., a database).
96+
res.cookie("access_token", data.access_token, { maxAge: 60 * 60 * 1000 });
97+
res.cookie("refresh_token", data.refresh_token, {
98+
// Prevent client-side scripts from accessing this data.
99+
httpOnly: true,
100+
secure: true,
101+
});
102+
103+
// Redirect to the main page with the access token loaded into a URL query param.
104+
res.redirect(`/?access_token=${data.access_token}`);
105+
})
106+
.catch((err) => {
107+
console.log(err.message);
108+
});
109+
});
110+
111+
app.get("/get-me", (req, res) => {
112+
// This assumes that the access token exists and has NOT expired.
113+
if (req.cookies.access_token) {
114+
const config = {
115+
headers: {
116+
Authorization: "Bearer " + req.cookies.access_token,
117+
},
118+
};
119+
120+
// Below, we are making a request to GET /users/me (docs: https://developers.asana.com/reference/getuser)
121+
//
122+
// If the request returns a 401 Unauthorized status, you should refresh your access token (not shown).
123+
// You can do so by making another request to the token exchange endpoint, this time passing in
124+
// a 'refresh_token' parameter (whose value is the actual refresh token), and also setting
125+
// 'grant_type' to 'refresh_token' (instead of 'authorization_code').
126+
//
127+
// Docs: https://developers.asana.com/docs/oauth#token-exchange-endpoint
128+
//
129+
// If using Axios, you can implement a refresh token mechanism with interceptors (docs: https://axios-http.com/docs/interceptors).
130+
axios
131+
.get("https://app.asana.com/api/1.0/users/me?opt_pretty=true", config)
132+
.then((res) => res.data)
133+
.then((userInfo) => {
134+
console.log("***** Response from GET /users/me:\n");
135+
console.log(JSON.stringify(userInfo, null, 2));
136+
137+
// Send back a JSON response from GET /users/me as JSON (viewable in the browser).
138+
res.json(userInfo);
139+
});
140+
} else {
141+
res.redirect("/");
142+
}
143+
});
144+
145+
// Start server on port 3000.
146+
app.listen(3000, () =>
147+
console.log("Server started -> http://localhost:3000\n")
148+
);

0 commit comments

Comments
 (0)