Skip to content

Add authentication sample using web application flow #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 13, 2020
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Base Url endpoint
BASE_URL = 'https://api-sandbox.uphold.com'

CLIENT_ID = ''
CLIENT_SECRET = ''
SERVER_PORT = 3000
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
node_modules/
52 changes: 52 additions & 0 deletions rest-api/javascript/authentication/web-application-flow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Authorization code flow

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

## Summary

This flow is **recommended for web applications** that wish to retrieve information about a user's Uphold account,
or take actions on their behalf.

This process, sometimes called "3-legged OAuth", requires three steps, each initiated by one of the three actors:

1. The **user** navigates to Uphold's website, following an authorization URL generated by the app,
where they log in and authorize the app to access their Uphold account;
2. **Uphold** redirects the user back to the app's website, including a short-lived authorization code in the URL;
3. The **application**'s server submits this code to Uphold's API, obtaining a long-lived access token in response.
(Since this final step occurs in server-to-server communication, the actual code is never exposed to the browser.)

This example sets up a local server that can be used to perform the OAuth web application flow cycle as described above.

## Requirements

To run this example, you must have:

- Node.js v13.14.0 or later
- An account at <https://sandbox.uphold.com>

## Setup

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

## Run

- Run `node index.js`
- Open the URL printed in the command line.
- **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.
- Click the link in the page to navigate to Uphold's servers.
- Accept the application's permission request.

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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Dependencies.
*/

import axios from "axios";
import b64Pkg from "js-base64";
import dotenv from "dotenv";
import qs from "qs";
import path from "path";

const { encode } = b64Pkg;

/**
* Dotenv configuration.
*/

dotenv.config({ path: path.resolve() + "/.env" });

/**
* Compose error page.
*/

export function composeErrorPage(data, state) {
let content = "<h1>Something went wrong.</h1>";

if (data.state && data.state !== state) {
content +=
`<p>The received state (<code>${data.state}</code>)
does not match the expected value: <code>${state}</code>.</p>`;
} else if (Object.values(data).length) {
content += "<p>Here's what Uphold's servers returned:</p>";
content += `<pre>${JSON.stringify(data, null, 4)}</pre>`;
} else {
content += "<p>This page should be reached at the end of an OAuth authorization process.</p>";
content += "<p>Please confirm that you followed the steps in the README.</p>";
}

return content;
}

/**
* Get assets.
*/

export async function getAssets(token) {
try {
const response = await axios.get(`${process.env.BASE_URL}/v0/assets`, {
headers: {
Authorization: `${token.token_type} ${token.access_token}`,
},
});
return response.data;
} catch (error) {
console.log(JSON.stringify(error, null, 2));
throw error;
}
}

/**
* Get Token.
*/

export async function getToken(code) {
// Base64-encoded authentication credentials
const auth = encode(process.env.CLIENT_ID + ":" + process.env.CLIENT_SECRET);

// set POST options for Axios
const options = {
method: "POST",
headers: {
Authorization: "Basic " + auth,
"content-type": "application/x-www-form-urlencoded",
},
data: qs.stringify({ code, grant_type: "client_credentials" }),
url: `${process.env.BASE_URL}/oauth2/token`,
};

const data = axios(options)
.then((response) => {
return response.data;
})
.catch((error) => {
error.response.data.errors
? console.log(JSON.stringify(error.response.data.errors, null, 2))
: console.log(JSON.stringify(error, null, 2));
throw error;
});

return data;
}
27 changes: 27 additions & 0 deletions rest-api/javascript/authentication/web-application-flow/cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE-----
MIIEljCCAn4CCQCHHmqArzJctTANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJw
dDAeFw0yMDEwMjIxMzE1MTlaFw0yMTEwMjIxMzE1MTlaMA0xCzAJBgNVBAYTAnB0
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArsxVWb12Rmn7oocPIXe5
SgiiRAqS4pZ2jnpxkN2rYp8sBBZCYAkRFNfIh+SUoOVPUhrM5JDZ9stmPHAba/XO
oOhhq4paI082TqBiBqqjYtofqdKp3FjMLXcV+Y1jYnphZ6U+eZKsLUc0hWCK/TFa
6H5Y3EUip9t8xmNRkzo2xhd5gNTEsvFMx/1XzJVJ7T5kgjKF+SECEw3leQva1RPK
g7+REF1McZlWqav8Y/qEQ3ZS1AIJLEHG0x1rSE9zf0PHiTYecPbYYLwMccHorKua
owjQKt9MboFN5u+Ne99sTyLuemDBmBFEI87ecBBCDzQb/bOQoqKJDY4u1xjO5Crf
tvVgZoGkwlXobJZ4r02Zfh338dXCIbTw9tXNV0sGtdiDMD3uPDq7+pU7mjWIcWjb
1hNVFxOO5Bna42r8q53QkfibKTVEZZaZmOu9vosOBGa4YsYUXvh0N1TqS7jNYv2H
vTdKYZnJvEoFj4bXEpyA8Dk/roybij19l0d5w6SR34Aq1M63NxGwph4CiCJ2SMGU
u+y048/XH64Bn1GjE19yyZ8JKi6tiENY6m9WS4BDGFiAOL5XkthcUmI5j8yaHncr
iSIJmfEiwL3ZCiUzD3Ua3l5oSK+aG5hf7FU9rXNYnt6byerrZasZnGi8U4y4CzLA
VEdfuklz3fBUVqz2fU6cFEkCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAW9j6giBn
iiphY4GMuQlQr3mf/rrtqCDIV+SAkhi/IzKbk4x/5yXoLZ2r9FbcZmNigBjQqB16
V0YN2lNYFiSF9Sx9Qv8XIFXYyPIKucHKekGASDk8oqmPtHQYBH9hOTRN8SRaT7aQ
rV1pYqdkjjG4gtjauYTAXucgQjP7d4kj8jOadZCffN53/6ASPRkj/Q+vpUlj0dxd
tjrEi1NxwbHahi59UggTg6ftLTgIMOHJYWMyTuR++B8m+UT6bFpPxB5enfcL+Qg9
4cTK0MtebyIIXmXv2L5S56/En+Kvlq3ynRFlqq9kdHK80kqjmPw6D2A+RHka1nDb
uo61ZPxBznMk9s8SJix+lv3MvinOJCiJDjYhef0rZXSSUEmXa58IF7iZdV+SIlUp
bEbEpCvVqBgc8XDoVcSp96rpZDSuSYfU7Xz9McyFbOtq+NkEtDevxE8r3WqIBh9x
efss+CBkrdGyj5qyBTd8YyLKvY3fsPfS08BMN7cMZVw8wsICymAGUFHk5Do3RFxM
tgD1VE26v0cluQwguYWZgRLR9lK1vREs7OfRb4RaXLczArOza5o4JTwmPByZ7owT
PzK3H9ydn9oq8LoRoY+9s3IRgdRQSD/idf/QylsZ9Es4av9LO+6pvmO+Sr/rnIQb
Gg6t7OPvwYNi62kS7eywQNbbfIB6wX0h/iw=
-----END CERTIFICATE-----
81 changes: 81 additions & 0 deletions rest-api/javascript/authentication/web-application-flow/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Dependencies.
*/

import dotenv from "dotenv";
import express from "express";
import fs from "fs";
import https from "https";
import path from "path";
import { randomBytes } from "crypto";
import { composeErrorPage, getAssets, getToken } from "./authorization-code-flow.js";

/**
* Dotenv configuration.
*/

dotenv.config({ path: path.resolve() + "/.env" });

/**
* Server configuration.
*/

const app = express();
const port = process.env.SERVER_PORT || 3000;
const state = randomBytes(8).toString('hex');

/**
* Main page.
*/

app.get("/", async (req, res) => {
// Compose the authorization URL. This assumes the `user:read` scope has been activated for this application.
const authorizationUrl = 'https://sandbox.uphold.com/authorize/'
+ process.env.CLIENT_ID
+ '?scope=user:read'
+ '&state=' + state;

res.send(
`<h1>Demo app server</h1>
<p>Please <a href="${authorizationUrl}">authorize this app</a> on Uphold's Sandbox.</p>`
);
});


/**
* Callback URL endpoint.
*/

app.get("/callback", async (req, res) => {
// Show an error page if the code wasn't returned or the state doesn't match what we sent.
if (!req.query.code || req.query.state !== state) {
res.send(composeErrorPage(req.query, state));
}

// Exchange the short-lived authorization code for a long-lived access token.
const token = await getToken(req.query.code);
console.log(`Authorization code ${req.query.code} successfully exchanged for access token:`, token);

// Test the new token by making a call to the API.
const assets = await getAssets(token);
console.log("Output from test API call:", assets[0]);

res.send(
`<h1>Success!</h1>
<p>The OAuth authorization code has been successfully exchanged for an access token.</p>`
);
});

/**
* Run server.
*/

https
.createServer({
key: fs.readFileSync("./key.pem"),
cert: fs.readFileSync("./cert.pem"),
passphrase: "test",
}, app)
.listen(port, () => {
console.log(`Server running at https://localhost:${port}`);
});
54 changes: 54 additions & 0 deletions rest-api/javascript/authentication/web-application-flow/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
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
idnWYlp+GmR0sckgYM6+f3R+PQ==
-----END ENCRYPTED PRIVATE KEY-----
Loading