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 idp endpoints to FTW server #1365

Merged
merged 3 commits into from
Oct 12, 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
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);
});
};
124 changes: 124 additions & 0 deletions server/api/auth/loginWithIdp.js
Original file line number Diff line number Diff line change
@@ -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#`);
});
};
4 changes: 4 additions & 0 deletions server/apiRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: ================ //
Expand Down Expand Up @@ -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;
17 changes: 17 additions & 0 deletions src/util/api.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
};
OtterleyW marked this conversation as resolved.
Show resolved Hide resolved