-
Notifications
You must be signed in to change notification settings - Fork 943
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1364 from sharetribe/social-logins
Add Facebook login to FTW
- Loading branch information
Showing
34 changed files
with
1,193 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}#`); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.