Skip to content

Commit

Permalink
Merge pull request #47 from Asana/aw-asana-node-oauth-demo
Browse files Browse the repository at this point in the history
Add OAuth Demo in JavaScript.
  • Loading branch information
aw-asana authored Mar 10, 2023
2 parents bd3c393 + cc32dd2 commit bf3af40
Show file tree
Hide file tree
Showing 12 changed files with 1,446 additions and 0 deletions.
4 changes: 4 additions & 0 deletions javascript/node-oauth-demo/.env-example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CLIENT_ID=753482910
CLIENT_SECRET=6572195638271537892521
REDIRECT_URI=http://localhost:3000/oauth-callback
COOKIE_SECRET=325797325
2 changes: 2 additions & 0 deletions javascript/node-oauth-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.env
76 changes: 76 additions & 0 deletions javascript/node-oauth-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# OAuth Demo

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).

_Note: This OAuth server should only be used for testing and learning purposes._

Documentation: Asana's [OAuth](https://developers.asana.com/docs/oauth)

## Requirements

The application was built with Node v19.4.0.

Visit [Node.js](https://nodejs.org/en/download/) get the latest version for your local machine.

## Installation

After cloning this project, navigate to the root directory and install dependencies:

```
npm i
```

## Usage

1. [Create an application](https://developers.asana.com/docs/oauth#register-an-application). Take note of your:

* Client ID
* Client secret
* Redirect URI

2. Create a `./env` file (in the root directory of the project) with the required configurations:

```
CLIENT_ID=your_client_id_here
CLIENT_SECRET=your_client_secret_here
REDIRECT_URI=your_redirect_uri_here
COOKIE_SECRET=can_be_any_value
```

You can view an example in the included `./env-example` file. Note that you should _never_ commit or otherwise expose your `./env` file publicly.

3. Start the server:

```
npm run dev
```

4. Visit [http://localhost:3000](http://localhost:3000) and click on "Authenticate with Asana"

![user auth screen](./images/mainscreen.png)

5. Select "Allow" to grant the application access to your Asana account

![user auth screen](./images/userauth.png)

You may also wish to view helpful outputs and notes in your terminal as well.

6. After successful authentication, you will be notified and redirected by the application

![user auth screen](./images/authedscreen.png)

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:

* 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)
* 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.)

## Deauthorizing the demo app

To remove the app from your list of Authorized Apps:

1. Click on your profile photo in the top right corner of the [Asana app](https://app.asana.com)
2. Select "My Settings"
3. Select the "App" tab
4. Select "Deauthorize" next to your application's name

Once deauthorized, you must begin the OAuth process again to authenticate with Asana.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added javascript/node-oauth-demo/images/mainscreen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added javascript/node-oauth-demo/images/userauth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
148 changes: 148 additions & 0 deletions javascript/node-oauth-demo/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
require("dotenv").config();
const axios = require("axios");
const express = require("express");
const path = require("path");
const cors = require("cors");
const cookieParser = require("cookie-parser");
const { v4: uuidv4 } = require("uuid");

const app = express();

// Enable CORS and assume the allowed origin is the redirect URI.
// i.e., this assumes that your client shares the same domain as the server.
app.use(
cors({
origin: "http://localhost:3000",
})
);

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

// Serve files in the ./static folder.
app.use(express.static("static"));

// Send static index.html page to the client.
// This page includes a button to authenticatate with Asana.
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "/static/index.html"));
});

// When the user clicks on the "Authenticate with Asana" button (from index.html),
// it redirects them to the user authorization endpoint.
// Docs: https://developers.asana.com/docs/oauth#user-authorization-endpoint
app.get("/authenticate", (req, res) => {
// Generate a `state` value and store it
// 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,
});

res.redirect(
`https://app.asana.com/-/oauth_authorize?response_type=code&client_id=${process.env.CLIENT_ID}&redirect_uri=${process.env.REDIRECT_URI}&state=${generatedState}`
);
});

// Redirect the user here upon successful or failed authentications.
// This endpoint on your server must be accessible via the redirect URL that you provided in the developer console.
// Docs: https://developers.asana.com/docs/oauth#register-an-application
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;
}

console.log(
"***** Code (to be exchanged for a token) and state from the user authorization response:\n"
);
console.log(`code: ${req.query.code}`);
console.log(`state: ${req.query.state}\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: process.env.REDIRECT_URI,
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,
});

// Redirect to the main page with the access token loaded into a URL query param.
res.redirect(`/?access_token=${data.access_token}`);
})
.catch((err) => {
console.log(err.message);
});
});

app.get("/get-me", (req, res) => {
// This assumes that the access token exists and has NOT expired.
if (req.cookies.access_token) {
const config = {
headers: {
Authorization: "Bearer " + req.cookies.access_token,
},
};

// Below, we are making a request to GET /users/me (docs: https://developers.asana.com/reference/getuser)
//
// If the request returns a 401 Unauthorized status, you should refresh your access token (not shown).
// You can do so by making another request to the token exchange endpoint, this time passing in
// a 'refresh_token' parameter (whose value is the actual refresh token), and also setting
// 'grant_type' to 'refresh_token' (instead of 'authorization_code').
//
// Docs: https://developers.asana.com/docs/oauth#token-exchange-endpoint
//
// If using Axios, you can implement a refresh token mechanism with interceptors (docs: https://axios-http.com/docs/interceptors).
axios
.get("https://app.asana.com/api/1.0/users/me?opt_pretty=true", config)
.then((res) => res.data)
.then((userInfo) => {
console.log("***** Response from GET /users/me:\n");
console.log(JSON.stringify(userInfo, null, 2));

// Send back a JSON response from GET /users/me as JSON (viewable in the browser).
res.json(userInfo);
});
} else {
res.redirect("/");
}
});

// Start server on port 3000.
app.listen(3000, () =>
console.log("Server started -> http://localhost:3000\n")
);
Loading

0 comments on commit bf3af40

Please sign in to comment.