Skip to content

Commit

Permalink
Move API router handlers into their own files
Browse files Browse the repository at this point in the history
  • Loading branch information
kpuputti committed May 18, 2020
1 parent 107a104 commit bbcd9f1
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 141 deletions.
28 changes: 28 additions & 0 deletions server/api-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const express = require('express');
const cookieParser = require('cookie-parser');

const PORT = 3500;
const app = express();
app.use(cookieParser());

app.get('/api/log', (req, res) => {
console.log('api-server log:', req.cookies);
res
.set({
'Access-Control-Allow-Origin': 'http://localhost:3000',
'Access-Control-Allow-Credentials': true,
})
.status(200)
.json({
req: {
method: req.method,
cookies: req.cookies || null,
cookieHeader: req.get('Cookie'),
},
})
.end();
});

app.listen(PORT, () => {
console.log(`api-server listening on ${PORT}`);
});
74 changes: 74 additions & 0 deletions server/api/initiate-login-as.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const crypto = require('crypto');

const CLIENT_ID = process.env.REACT_APP_SHARETRIBE_SDK_CLIENT_ID;
const ROOT_URL = process.env.REACT_APP_CANONICAL_ROOT_URL;
const CONSOLE_URL =
process.env.SERVER_SHARETRIBE_CONSOLE_URL || 'https://flex-console.sharetribe.com';
const USING_SSL = process.env.REACT_APP_SHARETRIBE_USING_SSL === 'true';

// redirect_uri param used when initiating a login as authentication flow and
// when requesting a token using an authorization code
const loginAsRedirectUri = `${ROOT_URL.replace(/\/$/, '')}/api/login-as`;

// Cookies used for authorization code authentication.
const stateKey = `st-${CLIENT_ID}-oauth2State`;
const codeVerifierKey = `st-${CLIENT_ID}-pkceCodeVerifier`;

/**
* Makes a base64 string URL friendly by
* replacing unaccepted characters.
*/
const urlifyBase64 = base64Str =>
base64Str
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');

// Initiates an authorization code authentication flow. This authentication flow
// enables marketplace operators that have an ongoing Console session to log
// into their marketplace as a user of the marketplace.
//
// The authorization code is requested from Console and it is used to request a
// token from the Flex Auth API.
//
// This endpoint will return a 302 to Console which requests the authorization
// code. Console returns a 302 with the code to the `redirect_uri` that is
// passed in this response. The request to the redirect URI is handled with the
// `/login-as` endpoint.
module.exports = (req, res) => {
const userId = req.query.user_id;

if (!userId) {
return res.status(400).send('Missing query parameter: user_id.');
}
if (!ROOT_URL) {
return res.status(409).send('Marketplace canonical root URL is missing.');
}

const state = urlifyBase64(crypto.randomBytes(32).toString('base64'));
const codeVerifier = urlifyBase64(crypto.randomBytes(32).toString('base64'));
const hash = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64');
const codeChallenge = urlifyBase64(hash);
const authorizeServerUrl = `${CONSOLE_URL}/api/authorize-as`;

const location = `${authorizeServerUrl}?\
response_type=code&\
client_id=${CLIENT_ID}&\
redirect_uri=${loginAsRedirectUri}&\
user_id=${userId}&\
state=${state}&\
code_challenge=${codeChallenge}&\
code_challenge_method=S256`;

const cookieOpts = {
maxAge: 1000 * 30, // 30 seconds
secure: USING_SSL,
};

res.cookie(stateKey, state, cookieOpts);
res.cookie(codeVerifierKey, codeVerifier, cookieOpts);
return res.redirect(location);
};
93 changes: 93 additions & 0 deletions server/api/login-as.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const http = require('http');
const https = require('https');
const sharetribeSdk = require('sharetribe-flex-sdk');
const Decimal = require('decimal.js');

const CLIENT_ID = process.env.REACT_APP_SHARETRIBE_SDK_CLIENT_ID;
const ROOT_URL = process.env.REACT_APP_CANONICAL_ROOT_URL;
const BASE_URL = process.env.REACT_APP_SHARETRIBE_SDK_BASE_URL;
const TRANSIT_VERBOSE = process.env.REACT_APP_SHARETRIBE_SDK_TRANSIT_VERBOSE === 'true';
const USING_SSL = process.env.REACT_APP_SHARETRIBE_USING_SSL === 'true';

// redirect_uri param used when initiating a login as authentication flow and
// when requesting a token using an authorization code
const loginAsRedirectUri = `${ROOT_URL.replace(/\/$/, '')}/api/login-as`;

// Instantiate HTTP(S) Agents with keepAlive set to true.
// This will reduce the request time for consecutive requests by
// reusing the existing TCP connection, thus eliminating the time used
// for setting up new TCP connections.
const httpAgent = new http.Agent({ keepAlive: true });
const httpsAgent = new https.Agent({ keepAlive: true });

// Cookies used for authorization code authentication.
const stateKey = `st-${CLIENT_ID}-oauth2State`;
const codeVerifierKey = `st-${CLIENT_ID}-pkceCodeVerifier`;

/**
* Makes a base64 string URL friendly by
* replacing unaccepted characters.
*/
const urlifyBase64 = base64Str =>
base64Str
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');

// Works as the redirect_uri passed in an authorization code request. Receives
// an authorization code and uses that to log in and redirect to the landing
// page.
module.exports = (req, res) => {
const { code, state, error } = req.query;
const storedState = req.cookies[stateKey];

if (state !== storedState) {
res.status(401).send('Invalid state parameter.');
return;
}

if (error) {
res.status(401).send(`Failed to authorize as a user, error: ${error}.`);
return;
}

const codeVerifier = req.cookies[codeVerifierKey];

// clear state and code verifier cookies
res.clearCookie(stateKey, { secure: USING_SSL });
res.clearCookie(codeVerifierKey, { secure: USING_SSL });

const baseUrl = BASE_URL ? { baseUrl: BASE_URL } : {};
const tokenStore = sharetribeSdk.tokenStore.expressCookieStore({
clientId: CLIENT_ID,
req,
res,
secure: USING_SSL,
});

const sdk = sharetribeSdk.createInstance({
transitVerbose: TRANSIT_VERBOSE,
clientId: CLIENT_ID,
httpAgent: httpAgent,
httpsAgent: httpsAgent,
tokenStore,
typeHandlers: [
{
type: sharetribeSdk.types.BigDecimal,
customType: Decimal,
writer: v => new sharetribeSdk.types.BigDecimal(v.toString()),
reader: v => new Decimal(v.value),
},
],
...baseUrl,
});

sdk
.login({
code,
redirect_uri: loginAsRedirectUri,
code_verifier: codeVerifier,
})
.then(() => res.redirect('/'))
.catch(() => res.status(401).send('Unable to authenticate as a user'));
};
145 changes: 4 additions & 141 deletions server/apiRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,151 +6,14 @@
* endpoints are prefixed in the main server where this file is used.
*/

const http = require('http');
const https = require('https');
const express = require('express');
const crypto = require('crypto');
const sharetribeSdk = require('sharetribe-flex-sdk');
const Decimal = require('decimal.js');

const CLIENT_ID = process.env.REACT_APP_SHARETRIBE_SDK_CLIENT_ID;
const ROOT_URL = process.env.REACT_APP_CANONICAL_ROOT_URL;
const CONSOLE_URL =
process.env.SERVER_SHARETRIBE_CONSOLE_URL || 'https://flex-console.sharetribe.com';
const BASE_URL = process.env.REACT_APP_SHARETRIBE_SDK_BASE_URL;
const TRANSIT_VERBOSE = process.env.REACT_APP_SHARETRIBE_SDK_TRANSIT_VERBOSE === 'true';
const USING_SSL = process.env.REACT_APP_SHARETRIBE_USING_SSL === 'true';
const initiateLoginAs = require('./api/initiate-login-as');
const loginAs = require('./api/login-as');

const router = express.Router();

// redirect_uri param used when initiating a login as authentication flow and
// when requesting a token using an authorization code
const loginAsRedirectUri = `${ROOT_URL.replace(/\/$/, '')}/api/login-as`;

// Instantiate HTTP(S) Agents with keepAlive set to true.
// This will reduce the request time for consecutive requests by
// reusing the existing TCP connection, thus eliminating the time used
// for setting up new TCP connections.
const httpAgent = new http.Agent({ keepAlive: true });
const httpsAgent = new https.Agent({ keepAlive: true });

// Cookies used for authorization code authentication.
const stateKey = `st-${CLIENT_ID}-oauth2State`;
const codeVerifierKey = `st-${CLIENT_ID}-pkceCodeVerifier`;

/**
* Makes a base64 string URL friendly by
* replacing unaccepted characters.
*/
const urlifyBase64 = base64Str =>
base64Str
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');

// Initiates an authorization code authentication flow. This authentication flow
// enables marketplace operators that have an ongoing Console session to log
// into their marketplace as a user of the marketplace.
//
// The authorization code is requested from Console and it is used to request a
// token from the Flex Auth API.
//
// This endpoint will return a 302 to Console which requests the authorization
// code. Console returns a 302 with the code to the `redirect_uri` that is
// passed in this response. The request to the redirect URI is handled with the
// `/login-as` endpoint.
router.get('/initiate-login-as', (req, res) => {
const userId = req.query.user_id;

if (!userId) {
return res.status(400).send('Missing query parameter: user_id.');
}
if (!ROOT_URL) {
return res.status(409).send('Marketplace canonical root URL is missing.');
}

const state = urlifyBase64(crypto.randomBytes(32).toString('base64'));
const codeVerifier = urlifyBase64(crypto.randomBytes(32).toString('base64'));
const hash = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64');
const codeChallenge = urlifyBase64(hash);
const authorizeServerUrl = `${CONSOLE_URL}/api/authorize-as`;

const location = `${authorizeServerUrl}?\
response_type=code&\
client_id=${CLIENT_ID}&\
redirect_uri=${loginAsRedirectUri}&\
user_id=${userId}&\
state=${state}&\
code_challenge=${codeChallenge}&\
code_challenge_method=S256`;

const cookieOpts = {
maxAge: 1000 * 30, // 30 seconds
secure: USING_SSL,
};

res.cookie(stateKey, state, cookieOpts);
res.cookie(codeVerifierKey, codeVerifier, cookieOpts);
return res.redirect(location);
});

// Works as the redirect_uri passed in an authorization code request. Receives
// an authorization code and uses that to log in and redirect to the landing
// page.
router.get('/login-as', (req, res) => {
const { code, state, error } = req.query;
const storedState = req.cookies[stateKey];

if (state !== storedState) {
return res.status(401).send('Invalid state parameter.');
}

if (error) {
return res.status(401).send(`Failed to authorize as a user, error: ${error}.`);
}

const codeVerifier = req.cookies[codeVerifierKey];

// clear state and code verifier cookies
res.clearCookie(stateKey, { secure: USING_SSL });
res.clearCookie(codeVerifierKey, { secure: USING_SSL });

const baseUrl = BASE_URL ? { baseUrl: BASE_URL } : {};
const tokenStore = sharetribeSdk.tokenStore.expressCookieStore({
clientId: CLIENT_ID,
req,
res,
secure: USING_SSL,
});

const sdk = sharetribeSdk.createInstance({
transitVerbose: TRANSIT_VERBOSE,
clientId: CLIENT_ID,
httpAgent: httpAgent,
httpsAgent: httpsAgent,
tokenStore,
typeHandlers: [
{
type: sharetribeSdk.types.BigDecimal,
customType: Decimal,
writer: v => new sharetribeSdk.types.BigDecimal(v.toString()),
reader: v => new Decimal(v.value),
},
],
...baseUrl,
});

sdk
.login({
code,
redirect_uri: loginAsRedirectUri,
code_verifier: codeVerifier,
})
.then(() => res.redirect('/'))
.catch(() => res.status(401).send('Unable to authenticate as a user'));
});
router.get('/initiate-login-as', initiateLoginAs);
router.get('/login-as', loginAs);

module.exports = router;

0 comments on commit bbcd9f1

Please sign in to comment.