From 84452ce3ea3dbaa9bb6479b5499719084d49a896 Mon Sep 17 00:00:00 2001 From: Eric Dobbertin Date: Sun, 29 Dec 2019 18:22:35 -0600 Subject: [PATCH 1/3] feat: implement change password flow Signed-off-by: Eric Dobbertin --- .env.example | 1 + src/components/AccountDropdown/AccountDropdown.js | 5 +++++ src/config.js | 1 + src/serverAuth.js | 15 +++++++++++++++ 4 files changed, 22 insertions(+) diff --git a/.env.example b/.env.example index d03c6b6099..bd29295abd 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,7 @@ OAUTH2_AUTH_URL=http://localhost:4444/oauth2/auth OAUTH2_CLIENT_ID=example-storefront OAUTH2_CLIENT_SECRET=CHANGEME OAUTH2_HOST=hydra.auth.reaction.localhost +OAUTH2_IDP_PUBLIC_CHANGE_PASSWORD_URL=http://localhost:4100/account/change-password?email=EMAIL&from=FROM OAUTH2_IDP_HOST_URL=http://identity.auth.reaction.localhost:4100 OAUTH2_REDIRECT_URL=http://localhost:4000/callback OAUTH2_TOKEN_URL=http://hydra.auth.reaction.localhost:4444/oauth2/token diff --git a/src/components/AccountDropdown/AccountDropdown.js b/src/components/AccountDropdown/AccountDropdown.js index cfce399ad6..68cc45344e 100644 --- a/src/components/AccountDropdown/AccountDropdown.js +++ b/src/components/AccountDropdown/AccountDropdown.js @@ -78,6 +78,11 @@ class AccountDropdown extends Component { Profile +
+ +
diff --git a/src/config.js b/src/config.js index 9e1b8bc41a..c768ba3f8c 100644 --- a/src/config.js +++ b/src/config.js @@ -37,6 +37,7 @@ if (process.env.IS_BUILDING_NEXTJS) { OAUTH2_AUTH_URL: url(), OAUTH2_CLIENT_ID: str(), OAUTH2_CLIENT_SECRET: str(), + OAUTH2_IDP_PUBLIC_CHANGE_PASSWORD_URL: url(), OAUTH2_IDP_HOST_URL: url(), OAUTH2_REDIRECT_URL: url(), OAUTH2_TOKEN_URL: url(), diff --git a/src/serverAuth.js b/src/serverAuth.js index 49619dd8f8..2d589fe7aa 100644 --- a/src/serverAuth.js +++ b/src/serverAuth.js @@ -74,6 +74,21 @@ function configureAuthForServer(server) { res.redirect(req.session.redirectTo || "/"); }); + server.get("/change-password", (req, res) => { + const { email } = req.query; + + let from = req.get("Referer"); + if (typeof from !== "string" || from.length === 0) { + from = config.CANONICAL_URL; + } + + let url = config.OAUTH2_IDP_PUBLIC_CHANGE_PASSWORD_URL; + url = url.replace("EMAIL", encodeURIComponent(email || "")); + url = url.replace("FROM", encodeURIComponent(from)); + + res.redirect(url); + }); + server.get("/logout/:userId", (req, res, next) => { const { userId } = req.params; if (!userId) { From 83e28f9e527e2dbc1f990bab7a33d9734ed106ed Mon Sep 17 00:00:00 2001 From: Eric Dobbertin Date: Mon, 30 Dec 2019 05:50:22 -0600 Subject: [PATCH 2/3] feat: use standard OpenID logout flow Signed-off-by: Eric Dobbertin --- .env.example | 2 +- .../AccountDropdown/AccountDropdown.js | 2 +- src/config.js | 2 +- src/serverAuth.js | 161 ++++++++++-------- 4 files changed, 95 insertions(+), 72 deletions(-) diff --git a/.env.example b/.env.example index bd29295abd..83fc6611b9 100644 --- a/.env.example +++ b/.env.example @@ -7,10 +7,10 @@ OAUTH2_ADMIN_URL=http://hydra.auth.reaction.localhost:4445 OAUTH2_AUTH_URL=http://localhost:4444/oauth2/auth OAUTH2_CLIENT_ID=example-storefront OAUTH2_CLIENT_SECRET=CHANGEME +OAUTH2_PUBLIC_LOGOUT_URL=http://localhost:4444/oauth2/sessions/logout OAUTH2_HOST=hydra.auth.reaction.localhost OAUTH2_IDP_PUBLIC_CHANGE_PASSWORD_URL=http://localhost:4100/account/change-password?email=EMAIL&from=FROM OAUTH2_IDP_HOST_URL=http://identity.auth.reaction.localhost:4100 -OAUTH2_REDIRECT_URL=http://localhost:4000/callback OAUTH2_TOKEN_URL=http://hydra.auth.reaction.localhost:4444/oauth2/token PORT=4000 SEGMENT_ANALYTICS_SKIP_MINIMIZE=true diff --git a/src/components/AccountDropdown/AccountDropdown.js b/src/components/AccountDropdown/AccountDropdown.js index 68cc45344e..958618464a 100644 --- a/src/components/AccountDropdown/AccountDropdown.js +++ b/src/components/AccountDropdown/AccountDropdown.js @@ -83,7 +83,7 @@ class AccountDropdown extends Component { Change Password - diff --git a/src/config.js b/src/config.js index c768ba3f8c..c67579f6b5 100644 --- a/src/config.js +++ b/src/config.js @@ -39,7 +39,7 @@ if (process.env.IS_BUILDING_NEXTJS) { OAUTH2_CLIENT_SECRET: str(), OAUTH2_IDP_PUBLIC_CHANGE_PASSWORD_URL: url(), OAUTH2_IDP_HOST_URL: url(), - OAUTH2_REDIRECT_URL: url(), + OAUTH2_PUBLIC_LOGOUT_URL: url(), OAUTH2_TOKEN_URL: url(), PORT: port({ default: 4000 }), SEGMENT_ANALYTICS_SKIP_MINIMIZE: bool({ default: false }), diff --git a/src/serverAuth.js b/src/serverAuth.js index 2d589fe7aa..d9239443d2 100644 --- a/src/serverAuth.js +++ b/src/serverAuth.js @@ -4,6 +4,29 @@ const config = require("./config"); const { decodeOpaqueId } = require("./lib/utils/decoding"); const logger = require("./lib/logger"); +let baseUrl = config.CANONICAL_URL; +if (!baseUrl.endsWith("/")) baseUrl = `${baseUrl}/`; + +const oauthRedirectUrl = `${baseUrl}callback`; +const oauthPostLogoutRedirectUrl = `${baseUrl}post-logout-callback`; + +/* eslint-disable camelcase */ +const storefrontHydraClient = { + client_id: config.OAUTH2_CLIENT_ID, + client_secret: config.OAUTH2_CLIENT_SECRET, + grant_types: [ + "authorization_code", + "refresh_token" + ], + post_logout_redirect_uris: [oauthPostLogoutRedirectUrl], + redirect_uris: [oauthRedirectUrl], + response_types: ["code", "id_token", "token"], + scope: "offline openid", + subject_type: "public", + token_endpoint_auth_method: "client_secret_post" +}; +/* eslint-enable camelcase */ + // This is needed to allow custom parameters (e.g. loginActions) to be included // when requesting authorization. This is setup to allow only loginAction to pass through OAuth2Strategy.prototype.authorizationParams = function (options = {}) { @@ -18,12 +41,12 @@ passport.use( tokenURL: config.OAUTH2_TOKEN_URL, clientID: config.OAUTH2_CLIENT_ID, clientSecret: config.OAUTH2_CLIENT_SECRET, - callbackURL: config.OAUTH2_REDIRECT_URL, + callbackURL: oauthRedirectUrl, state: true, - scope: ["offline"] + scope: ["offline", "openid"] }, - (accessToken, refreshToken, profile, cb) => { - cb(null, { accessToken }); + (accessToken, refreshToken, params, profile, cb) => { + cb(null, { accessToken, idToken: params.id_token }); } ) ); @@ -89,36 +112,25 @@ function configureAuthForServer(server) { res.redirect(url); }); - server.get("/logout/:userId", (req, res, next) => { - const { userId } = req.params; - if (!userId) { - next(); - return; + server.get("/logout", (req, res, next) => { + req.session.redirectTo = req.get("Referer"); + + const { idToken } = req.user || {}; + + // Clear storefront session auth + req.logout(); + + if (idToken) { + // Request log out of OAuth2 session + res.redirect(`${config.OAUTH2_PUBLIC_LOGOUT_URL}?post_logout_redirect_uri=${oauthPostLogoutRedirectUrl}&id_token_hint=${idToken}`); + } else { + res.redirect(req.session.redirectTo || config.CANONICAL_URL); } - const { id } = decodeOpaqueId(req.params.userId); - - let urlBase = config.OAUTH2_IDP_HOST_URL; - if (!urlBase.endsWith("/")) urlBase = `${urlBase}/`; - - // Ask IDP to log us out - fetch(`${urlBase}logout-user?userId=${id}`) - .then((logoutResponse) => { - if (logoutResponse.status >= 400) { - const message = `Error from OAUTH2_IDP_HOST_URL logout endpoint: ${logoutResponse.status}. Check the HOST server settings`; - - logger.error(message); - res.status(logoutResponse.status).send(message); - return; - } - // If IDP confirmed logout, clear login info on this side - req.logout(); - res.redirect(req.get("Referer") || "/"); - return; // appease eslint consistent-return - }) - .catch((error) => { - logger.error(`Error while logging out: ${error}`); - res.status(500).send(`Error while logging out: ${error.message}`); - }); + }); + + server.get("/post-logout-callback", (req, res) => { + // After success, redirect to the page we came from originally + res.redirect(req.session.redirectTo || "/"); }); } @@ -130,47 +142,58 @@ function configureAuthForServer(server) { * @returns {Promise} Nothing */ async function createHydraClientIfNecessary() { - /* eslint-disable camelcase */ - const bodyEncoded = JSON.stringify({ - client_id: config.OAUTH2_CLIENT_ID, - client_secret: config.OAUTH2_CLIENT_SECRET, - grant_types: [ - "authorization_code", - "refresh_token" - ], - jwks: {}, - redirect_uris: [config.OAUTH2_REDIRECT_URL], - response_types: ["token", "code"], - scope: "offline", - subject_type: "public", - token_endpoint_auth_method: "client_secret_post" - }); - /* eslint-enable camelcase */ - let adminUrl = config.OAUTH2_ADMIN_URL; if (!adminUrl.endsWith("/")) adminUrl = `${adminUrl}/`; - logger.info("Creating Hydra client..."); - - const response = await fetch(`${adminUrl}clients`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: bodyEncoded + const getClientResponse = await fetch(`${adminUrl}clients/${config.OAUTH2_CLIENT_ID}`, { + method: "GET", + headers: { "Content-Type": "application/json" } }); - switch (response.status) { - case 200: - // intentional fallthrough! - // eslint-disable-line no-fallthrough - case 201: - logger.info("OK: Hydra client created"); - break; - case 409: - logger.info("OK: Hydra client already exists"); - break; - default: - logger.error(await response.text()); - throw new Error(`Could not create Hydra client [${response.status}]`); + if (![200, 404].includes(getClientResponse.status)) { + logger.error(await getClientResponse.text()); + throw new Error(`Could not get Hydra client [${getClientResponse.status}]`); + } + + if (getClientResponse.status === 200) { + // Update the client to be sure it has the latest config + logger.info("Updating Hydra client..."); + + const updateClientResponse = await fetch(`${adminUrl}clients/${config.OAUTH2_CLIENT_ID}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(storefrontHydraClient) + }); + + if (updateClientResponse.status === 200) { + logger.info("OK: Hydra client updated"); + } else { + logger.error(await updateClientResponse.text()); + throw new Error(`Could not update Hydra client [${updateClientResponse.status}]`); + } + } else { + logger.info("Creating Hydra client..."); + + const response = await fetch(`${adminUrl}clients`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(storefrontHydraClient) + }); + + switch (response.status) { + case 200: + // intentional fallthrough! + // eslint-disable-line no-fallthrough + case 201: + logger.info("OK: Hydra client created"); + break; + case 409: + logger.info("OK: Hydra client already exists"); + break; + default: + logger.error(await response.text()); + throw new Error(`Could not create Hydra client [${response.status}]`); + } } } From 138282f928da60c2cb0317e870bb7a514e0c4ec5 Mon Sep 17 00:00:00 2001 From: Eric Dobbertin Date: Mon, 30 Dec 2019 14:09:06 -0600 Subject: [PATCH 3/3] chore: fix lint Signed-off-by: Eric Dobbertin --- src/serverAuth.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/serverAuth.js b/src/serverAuth.js index d9239443d2..3971e94544 100644 --- a/src/serverAuth.js +++ b/src/serverAuth.js @@ -1,7 +1,6 @@ const OAuth2Strategy = require("passport-oauth2"); const passport = require("passport"); const config = require("./config"); -const { decodeOpaqueId } = require("./lib/utils/decoding"); const logger = require("./lib/logger"); let baseUrl = config.CANONICAL_URL; @@ -112,7 +111,7 @@ function configureAuthForServer(server) { res.redirect(url); }); - server.get("/logout", (req, res, next) => { + server.get("/logout", (req, res) => { req.session.redirectTo = req.get("Referer"); const { idToken } = req.user || {};