Advanced External Logins
Since version 2.13 (Beta feature ~ API may change)
Extending external logins
Before extending external logins please refer to the External Logins docs to see what Front-Commerce has out of the box.
The external logins is made of three parts:
- The login providers. Responsible for handling authenticating the user with the external systems.
- The external login handlers responsible to logging authorized user into our platform (Magento, Proximis, BigCommerce...).
- A module to link the providers and the external login handler.
Creating your own provider
Front-Commerce uses passport internally for the Facebook and Google providers though this makes things easier it is neither a requirement nor a recommendation.
To create a provider you need to register a key and a function with the
auth-external-login
module.
The key will be used in the redirect url (like "google" in
/external-login/google
and /external-login/google/callback
) and the function
will be called to create the provider.
The function will be called with 3 arguments:
- The current request
- The externalLoginHandler
- The redirectUrl
The provider returned by the registered function should implement the
LoginProviderInterface
i.e. it should have 2 middlewares
requestLoginMiddleware
and loginCallbackMiddleware
.
The requestLoginMiddleware
will be called both when the user clicks on the
"Login with Provider" button and when the provider redirects back to the site.
The role of requestLoginMiddleware
is to first redirect the user to the
external site to authenticate and later when the user is redirected back to the
site (from the external system) to extract the user information.
The loginCallbackMiddleware
is called after the user is redirected back to the
site (from the external system) and also after requestLoginMiddleware
's second
call by this time requestLoginMiddleware
would have hopefully extracted all
needed user fields and made them available for loginCallbackMiddleware
.
(We/passort does it by exposing a field called user
on the request object
itself). Here is where the external login handler is used to log the user in.
import configService from "server/core/config/configService";
import { getCurrentShopConfig } from "server/core/config/getShopConfig";
import customConfigProvider from "path_to_your_customConfigProvider";
import { registerExternalLoginProvider } from "auth-external-login/server/providers";
configService.append(customConfigProvider);
const makeCustomProvider = (request, externalLoginHandler, callbackUrl) => {
const providerConfig = request.config.customProviderConfig;
if (!providerConfig.clientId || !providerConfig.clientSecret) {
throw new Error(
`Custom provider is not properly configured. clientId and clientSecret are required!`
);
}
const shopUrl = getCurrentShopConfig(req.config).url; // frequently needed to keep current store selected upon redirection
return new CustomProvider(externalLoginHandler, providerConfig, {
shopUrl,
callbackUrl,
});
};
registerExternalLoginProvider("custom", makeCustomProvider);
The LoginProviderBase
By implementing the Google and Facebook providers using passport these patterns emerged:
- The
loginCallbackMiddleware
is just a call to the external login handler'slogin
function. - The provider commonly needs:
- The login handler
- The provider's own config
- The shop and the callback URLs
LoginProviderBase
is a simple implementation of the LoginProviderInterface
that has the constructor (see below for signature) and the
loginCallbackMiddleware
implemented.
constructor(externalLoginHandler, providerConfig, { shopUrl, callbackUrl }) {
...
}
What is left is implementing requestLoginMiddleware
which is easily doable
using passport:
requestLoginMiddleware(req, res, next) {
const passportAuthenticator = new Passport();
passportAuthenticator.use(
new FacebookStrategy(
{
clientID: this.providerConfig.clientId,
clientSecret: this.providerConfig.clientSecret,
callbackURL: `${this.shopUrl}${this.callbackUrl}`,
profileFields: ["email", "name"],
},
(_, __, profile, cb) => {
const {
email,
first_name: firstname,
last_name: lastname,
} = profile._json;
if (!(email && firstname && lastname)) {
cb(new MissingInformationError());
} else {
cb(null, new ExternalLoginUser(email, firstname, lastname));
}
}
)
);
passportAuthenticator.authenticate("facebook", {
failureRedirect: `${this.shopUrl}/login`,
session: false,
scope: ["email"],
})(req, res, next);
}
Creating your own ExternalLoginHandler
The ExternalLoginHandler
is responsible of actually logging the user into your
platform. It should first create the user if he does not exist. Next it should
log in the user (old or new) into the system.
The external loging handler should implement the ExternalLoginHandlerInterface
interface which means that it must have a method called login
(see signature
below) that when called will ensure the user exist or otherwise create him and
finally log him/her in.
class CustomExternalLoginHander extends ExternalLoginHandlerInterface {
async login(request, user) {
// ensure/create then login user
}
}
We also provide a ExternalLoginHandlerBase
class that implements the
ExternalLoginHandlerInterface
. It only implements a constructor
with the
folowing signature:
constructor(globalConfig, staticConfigFromProviders) {
}
globalConfig
, staticConfigFromProviders
are used since they are what is sent
to the makeExternalLoginEndpoint
(see below), and for the globalConfig
you
can use it as follows:
import ExternalLoginHandlerBase from "auth-external-login/server/ExternalLoginHandlerBase";
import makeLoadersFromRequest from "server/core/graphql/makeLoadersFromRequest";
export default class CustomLoginHandler extends ExternalLoginHandlerBase {
async login(req, user) {
const loaders = makeLoadersFromRequest(this.globalConfig, req);
// you have access to all your loaders!!! like loaders.Product, loaders.Customer...
}
}
Putting it all together
As discussed previously the providers provide the middleware to handle the login
with the external system, and the external login handler handles actually
logging they user in. Now we are going to link the providers with the external
login handler in module that we can register into .front-commerce.js
A Front-Commerce module is basically a directory that follows certain
conventions. It can be placed any where in your folder structure but is commonly
placed either under ./modules/your_custom_module_name
or
./src/modules/your_custom_module_name
.
For our purposes we are only interensted in the server part of the module so we
need to create your_custom_module_name/server/module.config.js
file.
The your_custom_module_name/server/module.config.js
file should export an
object with the providers middleware as follows:
import makeExternalLoginEndpoint from "auth-external-login/server/makeExternalLoginEndpoint";
import CustomExternalLoginHandler from "you_external_login_handler/CustomExternalLoginHandler";
export default {
endpoint: makeExternalLoginEndpoint(
(config, staticConfigFromProviders) =>
new CustomExternalLoginHandler(/** some parameters you extract from the configs */)
),
};
This is pretty much all that is needed. Typically the ExternalLoginHandler
in
placed inside the same module but this is just a recomendation and not a
requirement.
Now you can register the new custom module in your .front-commerce.js
file and
do not forget to also register the base auth-external-login
module.
module.exports = {
name: "Front-Commerce DEV",
url: "http://www.front-commerce.test",
modules: [
"./node_modules/front-commerce/theme-chocolatine",
"./node_modules/front-commerce/modules/auth-external-login",
"./modules/my-custom-external-login",
],
};