Skip to content

Commit 0d6ca03

Browse files
committed
WIP
1 parent a1b5f0e commit 0d6ca03

File tree

8 files changed

+1427
-1253
lines changed

8 files changed

+1427
-1253
lines changed

rest-api/javascript/authentication/web-application-flow/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Base Url endpoint
2-
BASE_URL='https://api-sandbox.uphold.com'
2+
BASE_URL = 'https://api-sandbox.uphold.com'
33

44
CLIENT_ID = ''
55
CLIENT_SECRET = ''
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.env
2+
node_modules/
Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,41 @@
1-
# Client credentials web-flow
1+
# Authorization code flow
22

3-
This sample project demonstrates how to authenticate in the Uphold API using the client credentials web flow. For further background, please refer to the [API documentation](https://uphold.com/en/developer/api/documentation)
3+
This sample project demonstrates how a registered app can request authorization from Uphold users to perform actions on their behalf, by using the [authorization code OAuth flow](https://oauth.net/2/grant-types/authorization-code/). For further background, please refer to the [API documentation](https://uphold.com/en/developer/api/documentation).
44

55
## Summary
6-
**Ideal for web applications** that wish to retrieve information about a user’s Uphold account or take actions on their behalf.
7-
This example tries to mimic the WEB-FLOW cycle. It creates a simple webserver and tries to run the entire cycle of authenticating against UPHOLD servers and return the token to this web server.
86

9-
```
10-
https://sandbox.uphold.com/authorize/PUT_HERE_YOUR_CLIENT_ID?scope=user:read&state=PUT_A_CODE_HERE_TO_IDENTIFY_THE_REQUEST
11-
```
7+
This flow is **recommended for web applications** that wish to retrieve information about a user's Uphold account, or take actions on their behalf.
8+
9+
This process, sometimes called "3-legged OAuth", requires three steps, each initiated by one of the three actors:
10+
11+
1. The **user** navigates to a particular URL in the Uphold website, where they log in and authorize the app identified in the URL;
12+
2. **Uphold**'s server sends a short-lived authorization code to the app's server;
13+
3. The **application**'s server submits this code to Uphold to exchange it for a long-lived access token.
14+
15+
This example sets up a local server that can be used to perform the OAuth web application flow cycle as described above.
1216

1317
## Requirements
14-
- `node` v13.14.0 +
18+
19+
To run this example, you must have:
20+
21+
- Node.js v13.14.0 or later
22+
- An account at <https://sandbox.uphold.com>
1523

1624
## Setup
17-
- run `npm install` (or `yarn install`)
18-
- create a `.env` file based on the `.env.example` file, and populate it with the required data
25+
26+
- Run `npm install` (or `yarn install`)
27+
- [Create an app on Uphold Sandbox](https://sandbox.uphold.com/dashboard/profile/applications/developer/new)
28+
with the redirect URI field set to `https://localhost:3000/callback` (you may use a different port number, if you prefer).
29+
Note that this demo expects at least the `user:read` scope to be activated.
30+
- Create a `.env` file based on the `.env.example` file, and populate it with the required data.
31+
Make sure to also update the `SERVER_PORT` if you changed it in the previous step.
1932

2033
## Run
21-
- run `node index.js`
34+
35+
- Run `node index.js`
36+
- Open the URL printed in the command line.
37+
- **Attention:** Since the certificate used in this demo is self-signed, not all browsers will allow navigating to the page. You can use Firefox or Safari, which will display a warning but allow you to proceed regardless. Alternatively, you can navigate to `chrome://flags/#allow-insecure-localhost` in Chromium-based browsers to toggle support for self-signed localhost certificates.
38+
- Click the link in the page to navigate to Uphold's servers.
39+
- Accept the application's permission request.
40+
41+
Once the authorization is complete and an access token is obtained, the local server will use it to make a test request to the Uphold API. The output will be printed in the command line.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* Dependencies.
3+
*/
4+
5+
import axios from "axios";
6+
import b64Pkg from "js-base64";
7+
import dotenv from "dotenv";
8+
import qs from "qs";
9+
import path from "path";
10+
11+
const { encode } = b64Pkg;
12+
13+
/**
14+
* Dotenv configuration.
15+
*/
16+
17+
dotenv.config({ path: path.resolve() + "/.env" });
18+
19+
/**
20+
* Compose error page.
21+
*/
22+
23+
export function composeErrorPage(data, state) {
24+
let content = "<h1>Something went wrong.</h1>";
25+
26+
if (data.state && data.state !== state) {
27+
content +=
28+
`<p>The received state (<code>${data.state}</code>)
29+
does not match the expected value: <code>${state}</code>.</p>`;
30+
} else if (Object.values(data).length) {
31+
content += "<p>Here's what Uphold's servers returned:</p>";
32+
content += `<pre>${JSON.stringify(data, null, 4)}</pre>`;
33+
} else {
34+
content += "<p>This page should be reached at the end of an OAuth authorization process.</p>";
35+
content += "<p>Please confirm that you followed the steps in the README.</p>";
36+
}
37+
38+
return content;
39+
}
40+
41+
/**
42+
* Get assets.
43+
*/
44+
45+
export async function getAssets(token) {
46+
try {
47+
const response = await axios.get(`${process.env.BASE_URL}/v0/assets`, {
48+
headers: {
49+
Authorization: `${token.token_type} ${token.access_token}`,
50+
},
51+
});
52+
return response.data;
53+
} catch (error) {
54+
console.log(JSON.stringify(error, null, 2));
55+
throw error;
56+
}
57+
}
58+
59+
/**
60+
* Get Token.
61+
*/
62+
63+
export async function getToken(code) {
64+
// Base64-encoded authentication credentials
65+
const auth = encode(process.env.CLIENT_ID + ":" + process.env.CLIENT_SECRET);
66+
67+
// set POST options for Axios
68+
const options = {
69+
method: "POST",
70+
headers: {
71+
Authorization: "Basic " + auth,
72+
"content-type": "application/x-www-form-urlencoded",
73+
},
74+
data: qs.stringify({ code, grant_type: "client_credentials" }),
75+
url: `${process.env.BASE_URL}/oauth2/token`,
76+
};
77+
78+
const data = axios(options)
79+
.then((response) => {
80+
return response.data;
81+
})
82+
.catch((error) => {
83+
error.response.data.errors
84+
? console.log(JSON.stringify(error.response.data.errors, null, 2))
85+
: console.log(JSON.stringify(error, null, 2));
86+
throw error;
87+
});
88+
89+
return data;
90+
}
Lines changed: 49 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,81 @@
11
/**
22
* Dependencies.
33
*/
4-
import axios from "axios";
5-
import b64Pkg from "js-base64";
4+
65
import dotenv from "dotenv";
76
import express from "express";
87
import fs from "fs";
98
import https from "https";
10-
import qs from "qs";
119
import path from "path";
10+
import { randomBytes } from "crypto";
11+
import { composeErrorPage, getAssets, getToken } from "./authorization-code-flow.js";
1212

13-
const { encode } = b64Pkg;
14-
dotenv.config({ path: path.resolve() + "/.env" });
13+
/**
14+
* Dotenv configuration.
15+
*/
1516

16-
// For testing purposes point your browser to the fallowing URL
17-
// https://sandbox.uphold.com/authorize/PUT_HERE_YOUR_CLIENT_ID?scope=user:read&state=PUT_A_CODE_STATE_HERE
17+
dotenv.config({ path: path.resolve() + "/.env" });
1818

1919
/**
20-
* Get assets.
20+
* Server configuration.
2121
*/
22-
async function getAssets(token) {
23-
try {
24-
const r = await axios.get(`${process.env.BASE_URL}/v0/assets`, {
25-
headers: {
26-
Authorization: `${token.token_type} ${token.access_token}`,
27-
},
28-
});
29-
return r.data;
30-
} catch (error) {
31-
console.log(JSON.stringify(error, null, 2));
32-
throw error;
33-
}
34-
}
22+
23+
const app = express();
24+
const port = process.env.SERVER_PORT || 3000;
25+
const state = randomBytes(8).toString('hex');
3526

3627
/**
37-
* Get Token.
28+
* Main page.
3829
*/
39-
async function getToken(code) {
40-
// auth encoded with client ID and Client Secret
41-
// set post options for axios
42-
43-
const auth = encode(process.env.CLIENT_ID + ":" + process.env.CLIENT_SECRET);
44-
const url = `${process.env.BASE_URL}/oauth2/token`;
45-
46-
const options = {
47-
method: "POST",
48-
headers: {
49-
Authorization: "Basic " + auth,
50-
"content-type": "application/x-www-form-urlencoded",
51-
},
52-
data: qs.stringify({ code, grant_type: "client_credentials" }),
53-
url,
54-
};
55-
56-
const data = axios(options)
57-
.then((response) => {
58-
return response.data;
59-
})
60-
.catch((error) => {
61-
error.response.data.errors
62-
? console.log(JSON.stringify(error.response.data.errors, null, 2))
63-
: console.log(JSON.stringify(error, null, 2));
64-
throw error;
65-
});
66-
67-
return data;
68-
}
6930

70-
const app = express();
71-
const hostname = "localhost";
72-
const port = process.env.USERNAME || 3000;
31+
app.get("/", async (req, res) => {
32+
// Compose the authorization URL. This assumes the `user:read` scope has been activated for this application.
33+
const authorizationUrl = 'https://sandbox.uphold.com/authorize/'
34+
+ process.env.CLIENT_ID
35+
+ '?scope=user:read'
36+
+ '&state=' + state;
37+
38+
res.send(
39+
`<h1>Demo app server</h1>
40+
<p>Please <a href="${authorizationUrl}">authorize this app</a> on Uphold's Sandbox.</p>`
41+
);
42+
});
43+
7344

7445
/**
75-
* Callback url endpoint.
46+
* Callback URL endpoint.
7647
*/
7748

7849
app.get("/callback", async (req, res) => {
79-
// Do we have a code?
80-
// WARNING!!!!! The code only works for 5 minutes
81-
if (req.query.code) {
82-
console.log(`code ${req.query.code}`);
83-
84-
const token = await getToken(req.query.code);
85-
const assets = await getAssets(token);
86-
87-
console.log(token);
88-
console.log(assets);
89-
res.send("All done !");
90-
} else {
91-
res.send(`Oops, something went wrong... did you pass the STATE?`);
50+
// Show an error page if the code wasn't returned or the state doesn't match what we sent.
51+
if (!req.query.code || req.query.state !== state) {
52+
res.send(composeErrorPage(req.query, state));
9253
}
54+
55+
// Exchange the short-lived authorization code for a long-lived access token.
56+
const token = await getToken(req.query.code);
57+
console.log(`Authorization code ${req.query.code} successfully exchanged for access token:`, token);
58+
59+
// Test the new token by making a call to the API.
60+
const assets = await getAssets(token);
61+
console.log("Output from test API call:", assets[0]);
62+
63+
res.send(
64+
`<h1>Success!</h1>
65+
<p>The OAuth authorization code has been successfully exchanged for an access token.</p>`
66+
);
9367
});
9468

9569
/**
9670
* Run server.
9771
*/
9872

9973
https
100-
.createServer(
101-
{
102-
key: fs.readFileSync("./key.pem"),
103-
cert: fs.readFileSync("./cert.pem"),
104-
passphrase: "test",
105-
},
106-
app
107-
)
74+
.createServer({
75+
key: fs.readFileSync("./key.pem"),
76+
cert: fs.readFileSync("./cert.pem"),
77+
passphrase: "test",
78+
}, app)
10879
.listen(port, () => {
109-
console.log(`Serving running at https://${hostname}:${port}/`);
80+
console.log(`Server running at https://localhost:${port}`);
11081
});

0 commit comments

Comments
 (0)