Skip to content

Commit 6378bb6

Browse files
Paulo Cabralwaldyrious
authored andcommitted
Add authentication sample using web application flow
1 parent 02e87b0 commit 6378bb6

File tree

9 files changed

+1585
-0
lines changed

9 files changed

+1585
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Base Url endpoint
2+
BASE_URL = 'https://api-sandbox.uphold.com'
3+
4+
CLIENT_ID = ''
5+
CLIENT_SECRET = ''
6+
SERVER_PORT = 3000
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: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Authorization code flow
2+
3+
This sample project demonstrates how a registered app can request authorization from Uphold users to perform actions on their behalf,
4+
by using the [authorization code OAuth flow](https://oauth.net/2/grant-types/authorization-code/).
5+
For further background, please refer to the [API documentation](https://uphold.com/en/developer/api/documentation).
6+
7+
## Summary
8+
9+
This flow is **recommended for web applications** that wish to retrieve information about a user's Uphold account,
10+
or take actions on their behalf.
11+
12+
This process, sometimes called "3-legged OAuth", requires three steps, each initiated by one of the three actors:
13+
14+
1. The **user** navigates to Uphold's website, following an authorization URL generated by the app,
15+
where they log in and authorize the app to access their Uphold account;
16+
2. **Uphold** redirects the user back to the app's website, including a short-lived authorization code in the URL;
17+
3. The **application**'s server submits this code to Uphold's API, obtaining a long-lived access token in response.
18+
(Since this final step occurs in server-to-server communication, the actual code is never exposed to the browser.)
19+
20+
This example sets up a local server that can be used to perform the OAuth web application flow cycle as described above.
21+
22+
## Requirements
23+
24+
To run this example, you must have:
25+
26+
- Node.js v13.14.0 or later
27+
- An account at <https://sandbox.uphold.com>
28+
29+
## Setup
30+
31+
- Run `npm install` (or `yarn install`)
32+
- [Create an app on Uphold Sandbox](https://sandbox.uphold.com/dashboard/profile/applications/developer/new)
33+
with the redirect URI field set to `https://localhost:3000/callback`
34+
(you may use a different port number, if you prefer).
35+
Note that this demo expects at least the `user:read` scope to be activated.
36+
- Create a `.env` file based on the `.env.example` file, and populate it with the required data.
37+
Make sure to also update the `SERVER_PORT` if you changed it in the previous step.
38+
39+
## Run
40+
41+
- Run `node index.js`
42+
- Open the URL printed in the command line.
43+
- **Attention:** Since the certificate used in this demo is self-signed, not all browsers will allow navigating to the page.
44+
You can use Firefox or Safari, which will display a warning but allow you to proceed regardless.
45+
Alternatively, you can navigate to `chrome://flags/#allow-insecure-localhost` in Chromium-based browsers,
46+
to toggle support for self-signed localhost certificates.
47+
- Click the link in the page to navigate to Uphold's servers.
48+
- Accept the application's permission request.
49+
50+
Once the authorization is complete and an access token is obtained,
51+
the local server will use it to make a test request to the Uphold API.
52+
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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIEljCCAn4CCQCHHmqArzJctTANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJw
3+
dDAeFw0yMDEwMjIxMzE1MTlaFw0yMTEwMjIxMzE1MTlaMA0xCzAJBgNVBAYTAnB0
4+
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArsxVWb12Rmn7oocPIXe5
5+
SgiiRAqS4pZ2jnpxkN2rYp8sBBZCYAkRFNfIh+SUoOVPUhrM5JDZ9stmPHAba/XO
6+
oOhhq4paI082TqBiBqqjYtofqdKp3FjMLXcV+Y1jYnphZ6U+eZKsLUc0hWCK/TFa
7+
6H5Y3EUip9t8xmNRkzo2xhd5gNTEsvFMx/1XzJVJ7T5kgjKF+SECEw3leQva1RPK
8+
g7+REF1McZlWqav8Y/qEQ3ZS1AIJLEHG0x1rSE9zf0PHiTYecPbYYLwMccHorKua
9+
owjQKt9MboFN5u+Ne99sTyLuemDBmBFEI87ecBBCDzQb/bOQoqKJDY4u1xjO5Crf
10+
tvVgZoGkwlXobJZ4r02Zfh338dXCIbTw9tXNV0sGtdiDMD3uPDq7+pU7mjWIcWjb
11+
1hNVFxOO5Bna42r8q53QkfibKTVEZZaZmOu9vosOBGa4YsYUXvh0N1TqS7jNYv2H
12+
vTdKYZnJvEoFj4bXEpyA8Dk/roybij19l0d5w6SR34Aq1M63NxGwph4CiCJ2SMGU
13+
u+y048/XH64Bn1GjE19yyZ8JKi6tiENY6m9WS4BDGFiAOL5XkthcUmI5j8yaHncr
14+
iSIJmfEiwL3ZCiUzD3Ua3l5oSK+aG5hf7FU9rXNYnt6byerrZasZnGi8U4y4CzLA
15+
VEdfuklz3fBUVqz2fU6cFEkCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAW9j6giBn
16+
iiphY4GMuQlQr3mf/rrtqCDIV+SAkhi/IzKbk4x/5yXoLZ2r9FbcZmNigBjQqB16
17+
V0YN2lNYFiSF9Sx9Qv8XIFXYyPIKucHKekGASDk8oqmPtHQYBH9hOTRN8SRaT7aQ
18+
rV1pYqdkjjG4gtjauYTAXucgQjP7d4kj8jOadZCffN53/6ASPRkj/Q+vpUlj0dxd
19+
tjrEi1NxwbHahi59UggTg6ftLTgIMOHJYWMyTuR++B8m+UT6bFpPxB5enfcL+Qg9
20+
4cTK0MtebyIIXmXv2L5S56/En+Kvlq3ynRFlqq9kdHK80kqjmPw6D2A+RHka1nDb
21+
uo61ZPxBznMk9s8SJix+lv3MvinOJCiJDjYhef0rZXSSUEmXa58IF7iZdV+SIlUp
22+
bEbEpCvVqBgc8XDoVcSp96rpZDSuSYfU7Xz9McyFbOtq+NkEtDevxE8r3WqIBh9x
23+
efss+CBkrdGyj5qyBTd8YyLKvY3fsPfS08BMN7cMZVw8wsICymAGUFHk5Do3RFxM
24+
tgD1VE26v0cluQwguYWZgRLR9lK1vREs7OfRb4RaXLczArOza5o4JTwmPByZ7owT
25+
PzK3H9ydn9oq8LoRoY+9s3IRgdRQSD/idf/QylsZ9Es4av9LO+6pvmO+Sr/rnIQb
26+
Gg6t7OPvwYNi62kS7eywQNbbfIB6wX0h/iw=
27+
-----END CERTIFICATE-----
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Dependencies.
3+
*/
4+
5+
import dotenv from "dotenv";
6+
import express from "express";
7+
import fs from "fs";
8+
import https from "https";
9+
import path from "path";
10+
import { randomBytes } from "crypto";
11+
import { composeErrorPage, getAssets, getToken } from "./authorization-code-flow.js";
12+
13+
/**
14+
* Dotenv configuration.
15+
*/
16+
17+
dotenv.config({ path: path.resolve() + "/.env" });
18+
19+
/**
20+
* Server configuration.
21+
*/
22+
23+
const app = express();
24+
const port = process.env.SERVER_PORT || 3000;
25+
const state = randomBytes(8).toString('hex');
26+
27+
/**
28+
* Main page.
29+
*/
30+
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+
44+
45+
/**
46+
* Callback URL endpoint.
47+
*/
48+
49+
app.get("/callback", async (req, res) => {
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));
53+
}
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+
);
67+
});
68+
69+
/**
70+
* Run server.
71+
*/
72+
73+
https
74+
.createServer({
75+
key: fs.readFileSync("./key.pem"),
76+
cert: fs.readFileSync("./cert.pem"),
77+
passphrase: "test",
78+
}, app)
79+
.listen(port, () => {
80+
console.log(`Server running at https://localhost:${port}`);
81+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
-----BEGIN ENCRYPTED PRIVATE KEY-----
2+
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
53+
idnWYlp+GmR0sckgYM6+f3R+PQ==
54+
-----END ENCRYPTED PRIVATE KEY-----

0 commit comments

Comments
 (0)