Thank you @nik.baleca!
For others who might struggle with setting up account linking with a custom UI using Express.js, here is how I managed to implement a behavior similar to the example with Auth0 SDK (auth0@4.4.0
) and express-openid-connect@2.17.1
.
Main app code:
import express from 'express';
import oidc from 'express-openid-connect';
app.use(oidc.auth({
authRequired: false, // Important for bypassing auth on /link route
secret: process.env.SECRET,
baseURL: process.env.BASE_URL, // Redirect URL
clientID: process.env.CLIENT_ID,
issuerBaseURL: process.env.ISSUER,
// Extract and store user in session (`req.appSession`)
afterCallback: (_req, _res, session) => {
session.user = jwtDecode(session.id_token);
return session;
}
}));
// Authenicated routes
app.use('/', oidc.requiresAuth(), homeRouter);
// Link route must not be authenticated with main OIDC config
app.use('/link', linkRouter);
// (...)
You may have as many authenticated routes using the requiresAuth()
middleware. One of these routes must lead the end-user to make a POST call to /link
. I used an HTML form rendered in a template with a single button. Here’s how it should be handled by the linkRouter
:
import express from 'express';
import oidc from 'express-openid-connect';
const linkRouter = express.Router();
// Secondary account OIDC config
linkRouter.use(oidc.auth({
authRequired: false,
secret: process.env.SECRET,
baseURL: process.env.BASE_URL + '/link', // Remember to register with your Auth0 application
clientID: process.env.CLIENT_ID,
issuerBaseURL: process.env.ISSUER,
authorizationParams: {
connection: 'connection-name', // Set whatever connection the secondary account is authenticated with
},
session: {
name: 'linkSession' // Save data to another session, otherwise appSession will be overwritten
},
routes: {
login: false,
logout: false,
postLogoutRedirect: process.env.BASE_URL // After linking, go back to home page
}
}));
// POST call from end-user (eg. through HTML form)
linkRouter.post(async (req, res, next) => {
// At this point user should already have authenticated with primary account,
// so a cookie has been sent and appSession has been populated.
if (!req.appSession?.user?.email) {
next(new Error('Invalid session'));
} else {
// Redirect user for authentication with target account
await res.oidc.login({
returnTo: '/link',
authorizationParams: {
login_hint: req.appSession.user.email
}
});
}
});
// GET call after user has authenticated with secondary account
linkRouter.get(oidc.requiresAuth(), async (req, res, next) => {
const primaryUserId = req.appSession.user.sub; // appSession should be set from cookie related to main account session
const targetUserId = req.oidc.idTokenClaims.sub;
try {
// Link accounts
await this.auth0Service.linkUser(primaryUserId, targetUserId);
} catch (e) {
next(e);
} finally {
// Log out from secondary account, navigate back to home
await res.oidc.logout();
}
});
export default linkRouter;
There might be a better solution, I’m open to critics. In the meantime this is the app we use to allow our end-users to link their accounts.