Skip to content

Commit

Permalink
Merge pull request #118 from sharetribe/update-v6.4.0-from-upstream
Browse files Browse the repository at this point in the history
Update v6.4.0 from upstream
  • Loading branch information
OtterleyW authored Oct 15, 2020
2 parents b5a3438 + 542a2f9 commit 3a0ef87
Show file tree
Hide file tree
Showing 36 changed files with 1,232 additions and 47 deletions.
5 changes: 5 additions & 0 deletions .env-template
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ SHARETRIBE_SDK_CLIENT_SECRET=
REACT_APP_SHARETRIBE_MARKETPLACE_CURRENCY=USD
REACT_APP_CANONICAL_ROOT_URL=http://localhost:3000

# Social logins && SSO
# If the app or client id is not set the auhtentication option is not shown in FTW
REACT_APP_FACEBOOK_APP_ID=
FACEBOOK_APP_SECRET=

# This is overwritten by configuration in .env.development and
# .env.test. In production deployments use env variable and set it to
# 'production'
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ https://github.com/sharetribe/flex-template-web/

## Upcoming version 2020-XX-XX

## [v8.4.0] 2020-10-15

### Updates from upstream (FTW-daily v6.4.0)

- [add] Add Facebook login as a first step towards supporting social logins and SSO in FTW. This PR
introduces new endpoints createUserWithIdp and loginWithIdp and strategy for logging in with
Facebook. See the PR for the more detailed view of the changes. #1364
- [fix] Fix missing proptype warnings in TransactionPage and TransactionPanel tests. #1363
- [fix] Improve error handling by passing error details forward instead of creating a new error that
hides the details when making API call to FTW server. #1361
- [fix] Remove duplicate page schema from body. #1355

[v8.4.0]: https://github.com/sharetribe/ftw-hourly/compare/v8.3.1...v8.4.0

## [v8.3.1] 2020-08-19

- [fix] Fix popup-button in SelectSingleFilterPopup.css and adjust Footer with correct baselines.
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "app",
"version": "8.3.1",
"version": "8.4.0",
"private": true,
"license": "Apache-2.0",
"dependencies": {
Expand Down Expand Up @@ -36,6 +36,8 @@
"moment-timezone": "^0.5.26",
"object.entries": "^1.1.2",
"object.values": "^1.1.1",
"passport": "^0.4.1",
"passport-facebook": "^3.0.0",
"path-to-regexp": "^6.1.0",
"prop-types": "^15.7.2",
"query-string": "^6.13.1",
Expand All @@ -54,7 +56,7 @@
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"seedrandom": "^3.0.5",
"sharetribe-flex-sdk": "1.12.0",
"sharetribe-flex-sdk": "1.13.0",
"sharetribe-scripts": "3.1.1",
"smoothscroll-polyfill": "^0.4.0",
"source-map-support": "^0.5.9",
Expand Down
9 changes: 6 additions & 3 deletions server/api-util/sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,16 @@ exports.deserialize = str => {
exports.handleError = (res, error) => {
console.error(error);
if (error.status && error.statusText && error.data) {
const { status, statusText, data } = error;

// JS SDK error
res
.status(error.status)
.json({
status: error.status,
statusText: error.statusText,
data: error.data,
name: 'Local API request failed',
status,
statusText,
data,
})
.end();
} else {
Expand Down
81 changes: 81 additions & 0 deletions server/api/auth/createUserWithIdp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const http = require('http');
const https = require('https');
const sharetribeSdk = require('sharetribe-flex-sdk');
const { handleError, serialize, typeHandlers } = require('../../api-util/sdk');

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

const FACBOOK_APP_ID = process.env.REACT_APP_FACEBOOK_APP_ID;
const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID;

const FACEBOOK_IDP_ID = 'facebook';
const GOOGLE_IDP_ID = 'google';

// 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 });

const baseUrl = BASE_URL ? { baseUrl: BASE_URL } : {};

module.exports = (req, res) => {
const tokenStore = sharetribeSdk.tokenStore.expressCookieStore({
clientId: CLIENT_ID,
req,
res,
secure: USING_SSL,
});

const sdk = sharetribeSdk.createInstance({
transitVerbose: TRANSIT_VERBOSE,
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
httpAgent,
httpsAgent,
tokenStore,
typeHandlers,
...baseUrl,
});

const { idpToken, idpId, ...rest } = req.body;

// Choose the idpClientId based on which authentication method is used.
const idpClientId =
idpId === FACEBOOK_IDP_ID ? FACBOOK_APP_ID : idpId === GOOGLE_IDP_ID ? GOOGLE_CLIENT_ID : null;

sdk.currentUser
.createWithIdp({ idpId: FACEBOOK_IDP_ID, idpClientId, idpToken, ...rest })
.then(() =>
// After the user is created, we need to call loginWithIdp endpoint
// so that the user will be logged in.
sdk.loginWithIdp({
idpId,
idpClientId: `${idpClientId}`,
idpToken: `${idpToken}`,
})
)
.then(apiResponse => {
const { status, statusText, data } = apiResponse;
res
.clearCookie('st-authinfo')
.status(status)
.set('Content-Type', 'application/transit+json')
.send(
serialize({
status,
statusText,
data,
})
)
.end();
})
.catch(e => {
handleError(res, e);
});
};
78 changes: 78 additions & 0 deletions server/api/auth/facebook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const passport = require('passport');
const passportFacebook = require('passport-facebook');
const loginWithIdp = require('./loginWithIdp');

const radix = 10;
const PORT = parseInt(process.env.REACT_APP_DEV_API_SERVER_PORT, radix);
const rootUrl = process.env.REACT_APP_CANONICAL_ROOT_URL;
const clientID = process.env.REACT_APP_FACEBOOK_APP_ID;
const clientSecret = process.env.FACEBOOK_APP_SECRET;

const FacebookStrategy = passportFacebook.Strategy;
let callbackURL = null;

const useDevApiServer = process.env.NODE_ENV === 'development' && !!PORT;

if (useDevApiServer) {
callbackURL = `http://localhost:${PORT}/api/auth/facebook/callback`;
} else {
callbackURL = `${rootUrl}/api/auth/facebook/callback`;
}

const strategyOptions = {
clientID,
clientSecret,
callbackURL,
profileFields: ['id', 'name', 'emails'],
passReqToCallback: true,
};

const verifyCallback = (req, accessToken, refreshToken, profile, done) => {
const { email, first_name, last_name } = profile._json;
const state = req.query.state;
const queryParams = JSON.parse(state);

const { from, defaultReturn, defaultConfirm } = queryParams;

const userData = {
email,
firstName: first_name,
lastName: last_name,
accessToken,
refreshToken,
from,
defaultReturn,
defaultConfirm,
};

done(null, userData);
};

// ClientId is required when adding a new Facebook strategy to passport
if (clientID) {
passport.use(new FacebookStrategy(strategyOptions, verifyCallback));
}

exports.authenticateFacebook = (req, res, next) => {
const from = req.query.from ? req.query.from : null;
const defaultReturn = req.query.defaultReturn ? req.query.defaultReturn : null;
const defaultConfirm = req.query.defaultConfirm ? req.query.defaultConfirm : null;

const params = {
...(!!from && { from }),
...(!!defaultReturn && { defaultReturn }),
...(!!defaultConfirm && { defaultConfirm }),
};

const paramsAsString = JSON.stringify(params);

passport.authenticate('facebook', { scope: ['email'], state: paramsAsString })(req, res, next);
};

// Use custom callback for calling loginWithIdp enpoint
// to log in the user to Flex with the data from Facebook
exports.authenticateFacebookCallback = (req, res, next) => {
passport.authenticate('facebook', function(err, user) {
loginWithIdp(err, user, req, res, clientID, 'facebook');
})(req, res, next);
};
126 changes: 126 additions & 0 deletions server/api/auth/loginWithIdp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
const http = require('http');
const https = require('https');
const sharetribeSdk = require('sharetribe-flex-sdk');
const sdkUtils = require('../../api-util/sdk');

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

// 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 });

const baseUrl = BASE_URL ? { baseUrl: BASE_URL } : {};

module.exports = (err, user, req, res, clientID, idpId) => {
if (err) {
console.error(err);

// Save error details to cookie so that we can show
// relevant information in the frontend
return res
.cookie(
'st-autherror',
{
status: err.status,
code: err.code,
message: err.message,
},
{
maxAge: 15 * 60 * 1000, // 15 minutes
}
)
.redirect(`${rootUrl}/login#`);
}

if (!user) {
console.error('Failed to fetch user details from identity provider!');

// Save error details to cookie so that we can show
// relevant information in the frontend
return res
.cookie(
'st-autherror',
{
status: 'Bad Request',
code: 400,
message: 'Failed to fetch user details from identity provider!',
},
{
maxAge: 15 * 60 * 1000, // 15 minutes
}
)
.redirect(`${rootUrl}/login#`);
}

const { from, defaultReturn, defaultConfirm } = user;

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

const sdk = sharetribeSdk.createInstance({
transitVerbose: TRANSIT_VERBOSE,
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
httpAgent,
httpsAgent,
tokenStore,
typeHandlers: sdkUtils.typeHandlers,
...baseUrl,
});

return sdk
.loginWithIdp({
idpId: 'facebook',
idpClientId: clientID,
idpToken: user ? user.accessToken : null,
})
.then(response => {
if (response.status === 200) {
// If the user was authenticated, redirect back to to LandingPage
// We need to add # to the end of the URL because otherwise Facebook
// login will add their defaul #_#_ which breaks the routing in frontend.

if (from) {
res.redirect(`${rootUrl}${from}#`);
} else {
res.redirect(`${rootUrl}${defaultReturn}#`);
}
}
})
.catch(() => {
// If authentication fails, we want to create a new user with idp
// For this we will need to pass some information to frontend so
// that we can use that information in createUserWithIdp api call.
// The createUserWithIdp api call is triggered from frontend
// after showing a confirm page to user

res.cookie(
'st-authinfo',
{
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
idpToken: `${user.accessToken}`,
idpId,
from,
},
{
maxAge: 15 * 60 * 1000, // 15 minutes
}
);

res.redirect(`${rootUrl}${defaultConfirm}#`);
});
};
19 changes: 19 additions & 0 deletions server/apiRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ const transactionLineItems = require('./api/transaction-line-items');
const initiatePrivileged = require('./api/initiate-privileged');
const transitionPrivileged = require('./api/transition-privileged');

const createUserWithIdp = require('./api/auth/createUserWithIdp');

const { authenticateFacebook, authenticateFacebookCallback } = require('./api/auth/facebook');

const router = express.Router();

// ================ API router middleware: ================ //
Expand Down Expand Up @@ -50,4 +54,19 @@ router.post('/transaction-line-items', transactionLineItems);
router.post('/initiate-privileged', initiatePrivileged);
router.post('/transition-privileged', transitionPrivileged);

// Create user with identity provider (e.g. Facebook or Google)
// This endpoint is called to create a new user after user has confirmed
// they want to continue with the data fetched from IdP (e.g. name and email)
router.post('/auth/create-user-with-idp', createUserWithIdp);

// Facebook authentication endpoints

// This endpoint is called when user wants to initiate authenticaiton with Facebook
router.get('/auth/facebook', authenticateFacebook);

// This is the route for callback URL the user is redirected after authenticating
// with Facebook. In this route a Passport.js custom callback is used for calling
// loginWithIdp endpoint in Flex API to authenticate user to Flex
router.get('/auth/facebook/callback', authenticateFacebookCallback);

module.exports = router;
Loading

0 comments on commit 3a0ef87

Please sign in to comment.