From c110b6a564a4ccee47ce412a764c79dbfca4c19c Mon Sep 17 00:00:00 2001 From: Jenni Laakso Date: Tue, 22 Sep 2020 10:41:02 +0300 Subject: [PATCH 1/3] Add loginWithIdp endpoint --- server/api/auth/loginWithIdp.js | 124 ++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 server/api/auth/loginWithIdp.js diff --git a/server/api/auth/loginWithIdp.js b/server/api/auth/loginWithIdp.js new file mode 100644 index 0000000000..d939c7e5e3 --- /dev/null +++ b/server/api/auth/loginWithIdp.js @@ -0,0 +1,124 @@ +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 + 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 + 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 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, + }); + + 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 (user.returnUrl) { + res.redirect(`${rootUrl}${user.returnUrl}#`); + } else { + res.redirect(`${rootUrl}/#`); + } + } + }) + .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: user.returnUrl, + }, + { + maxAge: 15 * 60 * 1000, // 15 minutes + } + ); + + res.redirect(`${rootUrl}/confirm#`); + }); +}; From 1db18ca8738de5991088a85648338d0be9572e58 Mon Sep 17 00:00:00 2001 From: Jenni Laakso Date: Tue, 22 Sep 2020 10:45:23 +0300 Subject: [PATCH 2/3] Add createUserWithIdp endpoint --- server/api/auth/createUserWithIdp.js | 81 ++++++++++++++++++++++++++++ server/apiRouter.js | 4 ++ 2 files changed, 85 insertions(+) create mode 100644 server/api/auth/createUserWithIdp.js diff --git a/server/api/auth/createUserWithIdp.js b/server/api/auth/createUserWithIdp.js new file mode 100644 index 0000000000..6270d674ec --- /dev/null +++ b/server/api/auth/createUserWithIdp.js @@ -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); + }); +}; diff --git a/server/apiRouter.js b/server/apiRouter.js index 1a09735cb5..e51bf5af54 100644 --- a/server/apiRouter.js +++ b/server/apiRouter.js @@ -16,6 +16,8 @@ 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 router = express.Router(); // ================ API router middleware: ================ // @@ -50,4 +52,6 @@ router.post('/transaction-line-items', transactionLineItems); router.post('/initiate-privileged', initiatePrivileged); router.post('/transition-privileged', transitionPrivileged); +router.post('/auth/create-user-with-idp', createUserWithIdp); + module.exports = router; From 5e1d608b2420bdcf796027f1a13b2bcf9b95b6b9 Mon Sep 17 00:00:00 2001 From: Jenni Laakso Date: Tue, 22 Sep 2020 13:12:12 +0300 Subject: [PATCH 3/3] util/api.js: Add createWithIdp endpoint --- src/util/api.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/util/api.js b/src/util/api.js index 9aecc39478..9dc5609313 100644 --- a/src/util/api.js +++ b/src/util/api.js @@ -1,3 +1,7 @@ +// These helpers are calling FTW's own server-side routes +// and nor directly calling Marketplace API or Integration API +// You can find these api endpoints from 'server/api/...' directory + import { types as sdkTypes, transit } from './sdkLoader'; import config from '../config'; import Decimal from 'decimal.js'; @@ -98,3 +102,16 @@ export const initiatePrivileged = body => { export const transitionPrivileged = body => { return post('/api/transition-privileged', body); }; + +// Create user with identity provider (e.g. Facebook or Google) +// +// If loginWithIdp api call fails and user can't authenticate to Flex with idp +// we will show option to create a new user with idp. +// For that user needs to confirm data fetched from the idp. +// After the confrimation, this endpoint is called to create a new user with confirmed data. +// +// See `server/api/auth/createUserWithIdp.js` to see what data should +// be sent in the body. +export const createUserWithIdp = body => { + return post('/api/auth/create-user-with-idp', body); +};