Skip to content

Commit

Permalink
Merge pull request #1376 from sharetribe/google-login
Browse files Browse the repository at this point in the history
Add Google login to FTW
  • Loading branch information
OtterleyW authored Nov 16, 2020
2 parents a42f0d0 + 26350af commit 10f9574
Show file tree
Hide file tree
Showing 17 changed files with 3,340 additions and 1,085 deletions.
3 changes: 3 additions & 0 deletions .env-template
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ REACT_APP_CANONICAL_ROOT_URL=http://localhost:3000
REACT_APP_FACEBOOK_APP_ID=
FACEBOOK_APP_SECRET=

REACT_APP_GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ way to update this template, but currently, we follow a pattern:

## Upcoming version 2020-XX-XX

- [add] Add support for Google login. This works in the same way as Facebook flow so you can check
the [Facebook PR](https://github.com/sharetribe/ftw-daily/pull/1364) for the more details.
[#1376](https://github.com/sharetribe/ftw-daily/pull/1376)
- [fix] Routes component got double rendered due to Redux container HOC. Because navigation could
happen twice, loadData was also called twice.
[#1380](https://github.com/sharetribe/ftw-daily/pull/1380)
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"object.values": "^1.1.1",
"passport": "^0.4.1",
"passport-facebook": "^3.0.0",
"passport-google-oauth": "^2.0.0",
"path-to-regexp": "^6.1.0",
"prop-types": "^15.7.2",
"query-string": "^6.13.1",
Expand All @@ -55,7 +56,7 @@
"redux-thunk": "^2.3.0",
"seedrandom": "^3.0.5",
"sharetribe-flex-sdk": "1.13.0",
"sharetribe-scripts": "3.1.1",
"sharetribe-scripts": "3.4.4",
"smoothscroll-polyfill": "^0.4.0",
"source-map-support": "^0.5.9",
"url": "^0.11.0"
Expand Down
4 changes: 3 additions & 1 deletion server/api-util/sdk.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const http = require('http');
const https = require('https');
const Decimal = require('decimal.js');
const log = require('../log');
const sharetribeSdk = require('sharetribe-flex-sdk');

const CLIENT_ID = process.env.REACT_APP_SHARETRIBE_SDK_CLIENT_ID;
Expand Down Expand Up @@ -53,7 +54,8 @@ exports.deserialize = str => {
};

exports.handleError = (res, error) => {
console.error(error);
log.error(error, 'local-api-request-failed', error.data);

if (error.status && error.statusText && error.data) {
const { status, statusText, data } = error;

Expand Down
2 changes: 1 addition & 1 deletion server/api/auth/createUserWithIdp.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ module.exports = (req, res) => {
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 })
.createWithIdp({ idpId, idpClientId, idpToken, ...rest })
.then(() =>
// After the user is created, we need to call loginWithIdp endpoint
// so that the user will be logged in.
Expand Down
2 changes: 1 addition & 1 deletion server/api/auth/facebook.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const verifyCallback = (req, accessToken, refreshToken, profile, done) => {
email,
firstName: first_name,
lastName: last_name,
accessToken,
idpToken: accessToken,
refreshToken,
from,
defaultReturn,
Expand Down
87 changes: 87 additions & 0 deletions server/api/auth/google.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
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_GOOGLE_CLIENT_ID;
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;

let callbackURL = null;

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

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

const strategyOptions = {
clientID,
clientSecret,
callbackURL,
passReqToCallback: true,
};

const verifyCallback = (req, accessToken, refreshToken, rawReturn, profile, done) => {
// We need to add additional parameter `rawReturn` to the callback
// so that we can access the id_token coming from Google
// With Google we want to use that id_token instead of accessToken in Flex
const idpToken = rawReturn.id_token;

const { email, given_name, family_name } = profile._json;
const state = req.query.state;
const queryParams = JSON.parse(state);

const { from, defaultReturn, defaultConfirm } = queryParams;

const userData = {
email,
firstName: given_name,
lastName: family_name,
idpToken,
from,
defaultReturn,
defaultConfirm,
};

done(null, userData);
};

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

exports.authenticateGoogle = (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('google', {
scope: [
'https://www.googleapis.com/auth/plus.login',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email',
],
state: paramsAsString,
})(req, res, next);
};

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

const CLIENT_ID = process.env.REACT_APP_SHARETRIBE_SDK_CLIENT_ID;
Expand All @@ -21,7 +22,7 @@ const baseUrl = BASE_URL ? { baseUrl: BASE_URL } : {};

module.exports = (err, user, req, res, clientID, idpId) => {
if (err) {
console.error(err);
log.error(err, 'fetching-user-data-from-idp-failed');

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

if (!user) {
console.error('Failed to fetch user details from identity provider!');
log.error(
new Error('Failed to fetch user details from identity provider'),
'fetching-user-data-from-idp-failed'
);

// Save error details to cookie so that we can show
// relevant information in the frontend
Expand Down Expand Up @@ -82,9 +86,9 @@ module.exports = (err, user, req, res, clientID, idpId) => {

return sdk
.loginWithIdp({
idpId: 'facebook',
idpId,
idpClientId: clientID,
idpToken: user ? user.accessToken : null,
idpToken: user.idpToken,
})
.then(response => {
if (response.status === 200) {
Expand All @@ -100,6 +104,10 @@ module.exports = (err, user, req, res, clientID, idpId) => {
}
})
.catch(() => {
console.log(
'Authenticating with idp failed. User needs to confirm creating sign up in frontend.'
);

// 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.
Expand All @@ -112,7 +120,7 @@ module.exports = (err, user, req, res, clientID, idpId) => {
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
idpToken: `${user.accessToken}`,
idpToken: user.idpToken,
idpId,
from,
},
Expand Down
11 changes: 11 additions & 0 deletions server/apiRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const transitionPrivileged = require('./api/transition-privileged');
const createUserWithIdp = require('./api/auth/createUserWithIdp');

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

const router = express.Router();

Expand Down Expand Up @@ -69,4 +70,14 @@ router.get('/auth/facebook', authenticateFacebook);
// loginWithIdp endpoint in Flex API to authenticate user to Flex
router.get('/auth/facebook/callback', authenticateFacebookCallback);

// Google authentication endpoints

// This endpoint is called when user wants to initiate authenticaiton with Google
router.get('/auth/google', authenticateGoogle);

// This is the route for callback URL the user is redirected after authenticating
// with Google. 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/google/callback', authenticateGoogleCallback);

module.exports = router;
7 changes: 7 additions & 0 deletions src/containers/AuthenticationPage/AuthenticationPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,13 @@
margin-left: 20px;
}

.socialButtonWrapper {
margin-bottom: 6px;
@media (--viewportMedium) {
margin-top: 8px;
}
}

.socialButtonsOr {
width: 100%;
height: 32px;
Expand Down
47 changes: 40 additions & 7 deletions src/containers/AuthenticationPage/AuthenticationPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { sendVerificationEmail } from '../../ducks/user.duck';
import { manageDisableScrolling } from '../../ducks/UI.duck';

import css from './AuthenticationPage.css';
import { FacebookLogo } from './socialLoginLogos';
import { FacebookLogo, GoogleLogo } from './socialLoginLogos';
import { isEmpty } from 'lodash';

export class AuthenticationPageComponent extends Component {
Expand Down Expand Up @@ -199,7 +199,7 @@ export class AuthenticationPageComponent extends Component {
});
};

const authWithFacebook = () => {
const getDefaultRoutes = () => {
const routes = routeConfiguration();
const baseUrl = apiBaseUrl();

Expand All @@ -215,9 +215,20 @@ export class AuthenticationPageComponent extends Component {
const defaultConfirm = pathByRouteName('ConfirmPage', routes);
const defaultConfirmParam = defaultConfirm ? `&defaultConfirm=${defaultConfirm}` : '';

return { baseUrl, fromParam, defaultReturnParam, defaultConfirmParam };
};
const authWithFacebook = () => {
const defaultRoutes = getDefaultRoutes();
const { baseUrl, fromParam, defaultReturnParam, defaultConfirmParam } = defaultRoutes;
window.location.href = `${baseUrl}/api/auth/facebook?${fromParam}${defaultReturnParam}${defaultConfirmParam}`;
};

const authWithGoogle = () => {
const defaultRoutes = getDefaultRoutes();
const { baseUrl, fromParam, defaultReturnParam, defaultConfirmParam } = defaultRoutes;
window.location.href = `${baseUrl}/api/auth/google?${fromParam}${defaultReturnParam}${defaultConfirmParam}`;
};

const idp = this.state.authInfo
? this.state.authInfo.idpId.replace(/^./, str => str.toUpperCase())
: null;
Expand All @@ -240,18 +251,27 @@ export class AuthenticationPageComponent extends Component {
inProgress={authInProgress}
onOpenTermsOfService={() => this.setState({ tosModalOpen: true })}
authInfo={this.state.authInfo}
idp={idp}
/>
</div>
);

// Social login buttons
const showSocialLogins = !!process.env.REACT_APP_FACEBOOK_APP_ID;
const showFacebookLogin = !!process.env.REACT_APP_FACEBOOK_APP_ID;
const showGoogleLogin = !!process.env.REACT_APP_GOOGLE_CLIENT_ID;
const showSocialLogins = showFacebookLogin || showGoogleLogin;

const facebookButtonText = isLogin ? (
<FormattedMessage id="AuthenticationPage.loginWithFacebook" />
) : (
<FormattedMessage id="AuthenticationPage.signupWithFacebook" />
);

const googleButtonText = isLogin ? (
<FormattedMessage id="AuthenticationPage.loginWithGoogle" />
) : (
<FormattedMessage id="AuthenticationPage.signupWithGoogle" />
);
const socialLoginButtonsMaybe = showSocialLogins ? (
<div className={css.idpButtons}>
<div className={css.socialButtonsOr}>
Expand All @@ -260,10 +280,23 @@ export class AuthenticationPageComponent extends Component {
</span>
</div>

<SocialLoginButton onClick={() => authWithFacebook()}>
<span className={css.buttonIcon}>{FacebookLogo}</span>
{facebookButtonText}
</SocialLoginButton>
{showFacebookLogin ? (
<div className={css.socialButtonWrapper}>
<SocialLoginButton onClick={() => authWithFacebook()}>
<span className={css.buttonIcon}>{FacebookLogo}</span>
{facebookButtonText}
</SocialLoginButton>
</div>
) : null}

{showGoogleLogin ? (
<div className={css.socialButtonWrapper}>
<SocialLoginButton onClick={() => authWithGoogle()}>
<span className={css.buttonIcon}>{GoogleLogo}</span>
{googleButtonText}
</SocialLoginButton>
</div>
) : null}
</div>
) : null;

Expand Down
Loading

0 comments on commit 10f9574

Please sign in to comment.