Skip to content
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

Add Facebook login #1366

Merged
merged 13 commits into from
Oct 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
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"moment": "^2.22.2",
"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 Down
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);
};
18 changes: 10 additions & 8 deletions server/api/auth/loginWithIdp.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ module.exports = (err, user, req, res, clientID, idpId) => {

// Save error details to cookie so that we can show
// relevant information in the frontend
res
return res
.cookie(
'st-autherror',
{
Expand All @@ -45,7 +45,7 @@ module.exports = (err, user, req, res, clientID, idpId) => {

// Save error details to cookie so that we can show
// relevant information in the frontend
res
return res
.cookie(
'st-autherror',
{
Expand All @@ -60,6 +60,8 @@ module.exports = (err, user, req, res, clientID, idpId) => {
.redirect(`${rootUrl}/login#`);
}

const { from, defaultReturn, defaultConfirm } = user;

const tokenStore = sharetribeSdk.tokenStore.expressCookieStore({
clientId: CLIENT_ID,
req,
Expand All @@ -78,7 +80,7 @@ module.exports = (err, user, req, res, clientID, idpId) => {
...baseUrl,
});

sdk
return sdk
.loginWithIdp({
idpId: 'facebook',
idpClientId: clientID,
Expand All @@ -90,10 +92,10 @@ module.exports = (err, user, req, res, clientID, idpId) => {
// 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}#`);
if (from) {
res.redirect(`${rootUrl}${from}#`);
} else {
res.redirect(`${rootUrl}/#`);
res.redirect(`${rootUrl}${defaultReturn}#`);
}
}
})
Expand All @@ -112,13 +114,13 @@ module.exports = (err, user, req, res, clientID, idpId) => {
lastName: user.lastName,
idpToken: `${user.accessToken}`,
idpId,
from: user.returnUrl,
from,
},
{
maxAge: 15 * 60 * 1000, // 15 minutes
}
);

res.redirect(`${rootUrl}/confirm#`);
res.redirect(`${rootUrl}${defaultConfirm}#`);
});
};
15 changes: 15 additions & 0 deletions server/apiRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ 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 @@ -52,6 +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);
OtterleyW marked this conversation as resolved.
Show resolved Hide resolved

// 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;
7 changes: 7 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const enforceSsl = require('express-enforces-ssl');
const path = require('path');
const sharetribeSdk = require('sharetribe-flex-sdk');
const sitemap = require('express-sitemap');
const passport = require('passport');
const auth = require('./auth');
const apiRouter = require('./apiRouter');
const renderer = require('./renderer');
Expand Down Expand Up @@ -145,6 +146,12 @@ if (!dev) {
}
}

// Initialize Passport.js (http://www.passportjs.org/)
// Passport is authentication middleware for Node.js
// We use passport to enable authenticating with
// a 3rd party identity provider (e.g. Facebook or Google)
app.use(passport.initialize());

// Server-side routes that do not render the application
app.use('/api', apiRouter);

Expand Down
33 changes: 33 additions & 0 deletions src/components/Button/Button.css
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,36 @@
.secondaryButton .checkmark {
stroke: var(--matterColorAnti);
}

/* Social logins && SSO buttons */

.socialButtonRoot {
@apply --marketplaceButtonStyles;
min-height: 48px;
background-color: var(--matterColorLight);

color: var(--matterColorDark);
font-weight: var(--fontWeightMedium);
font-size: 14px;

border: 1px solid #d2d2d2;
border-radius: 4px;

/* We need to add this to position the icon inside button */
position: relative;

@media (--viewportMedium) {
padding: 0;
}

&:hover,
&:focus {
background-color: var(--matterColorLight);
}
&:disabled {
background-color: var(--matterColorNegative);
}
}

.socialButton {
}
7 changes: 7 additions & 0 deletions src/components/Button/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,10 @@ export const InlineTextButton = props => {
return <Button {...props} rootClassName={classes} />;
};
InlineTextButton.displayName = 'InlineTextButton';

export const SocialLoginButton = props => {
const classes = classNames(props.rootClassName || css.socialButtonRoot, css.socialButton);
return <Button {...props} rootClassName={classes} />;
};

SocialLoginButton.displayName = 'SocialLoginButton';
2 changes: 1 addition & 1 deletion src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export { default as ValidationError } from './ValidationError/ValidationError';
// First components that include only atomic components //
//////////////////////////////////////////////////////////

export { default as Button, PrimaryButton, SecondaryButton, InlineTextButton } from './Button/Button';
export { default as Button, PrimaryButton, SecondaryButton, InlineTextButton, SocialLoginButton } from './Button/Button';
export { default as CookieConsent } from './CookieConsent/CookieConsent';
export { default as ImageCarousel } from './ImageCarousel/ImageCarousel';
export { default as ImageFromFile } from './ImageFromFile/ImageFromFile';
Expand Down
5 changes: 4 additions & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,14 @@ const siteInstagramPage = null;
// Facebook page is used in SEO schema (http://schema.org/Organization)
const siteFacebookPage = 'https://www.facebook.com/Sharetribe/';

// Social logins & SSO

// Note: Facebook app id is also used for tracking:
// Facebook counts shares with app or page associated by this id
// Currently it is unset, but you can read more about fb:app_id from
// https://developers.facebook.com/docs/sharing/webmasters#basic
// You should create one to track social sharing in Facebook
const facebookAppId = null;
const facebookAppId = process.env.REACT_APP_FACEBOOK_APP_ID;
OtterleyW marked this conversation as resolved.
Show resolved Hide resolved

const maps = {
mapboxAccessToken: process.env.REACT_APP_MAPBOX_ACCESS_TOKEN,
Expand Down
60 changes: 60 additions & 0 deletions src/containers/AuthenticationPage/AuthenticationPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,63 @@
.error {
@apply --marketplaceModalErrorStyles;
}

/* ================ Social logins & SSO ================ */

.signupWithIdpTitle {
@apply --marketplaceModalTitleStyles;
margin-top: 0;
margin-bottom: 0;
padding-top: 16px;
color: var(--matterColorDark);

@media (--viewportMedium) {
margin-top: 6px;
}
}

.confirmInfoText {
@apply --marketplaceH4FontStyles;
}

.buttonIcon {
position: absolute;
left: 0;
margin-left: 20px;
}

.socialButtonsOr {
width: 100%;
height: 32px;
margin: 28px 0 20px 0;
text-align: center;
position: relative;
background-color: var(--matterColorLight);

&:after {
content: '';
width: 100%;
border-bottom: solid 1px #d2d2d2;
position: absolute;
left: 0;
top: 50%;
z-index: 1;
}
}

@media (--viewportMedium) {
.socialButtonsOr {
height: 34px;
margin: 15px 0;
}
}

.socialButtonsOrText {
background-color: var(--matterColorLight);
width: auto;
display: inline-block;
z-index: 3;
padding: 0 20px 0 20px;
position: relative;
margin: 0;
}
Loading