Skip to content

Commit

Permalink
Merge pull request #637 from reactioncommerce/feat-aldeed-change-pass…
Browse files Browse the repository at this point in the history
…word

Change password and logout
  • Loading branch information
willopez authored Dec 31, 2019
2 parents d440385 + 138282f commit 8b3210f
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 73 deletions.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +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
Expand Down
7 changes: 6 additions & 1 deletion src/components/AccountDropdown/AccountDropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ class AccountDropdown extends Component {
Profile
</Button>
</div>
<Button color="primary" fullWidth href={`/logout/${account._id}`} variant="contained">
<div className={classes.marginBottom}>
<Button color="primary" fullWidth href={`/change-password?email=${encodeURIComponent(account.emailRecords[0].address)}`}>
Change Password
</Button>
</div>
<Button color="primary" fullWidth href="/logout" variant="contained">
Sign Out
</Button>
</Fragment>
Expand Down
3 changes: 2 additions & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ 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_PUBLIC_LOGOUT_URL: url(),
OAUTH2_TOKEN_URL: url(),
PORT: port({ default: 4000 }),
SEGMENT_ANALYTICS_SKIP_MINIMIZE: bool({ default: false }),
Expand Down
177 changes: 107 additions & 70 deletions src/serverAuth.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
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;
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 = {}) {
Expand All @@ -18,12 +40,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 });
}
)
);
Expand Down Expand Up @@ -74,36 +96,40 @@ function configureAuthForServer(server) {
res.redirect(req.session.redirectTo || "/");
});

server.get("/logout/:userId", (req, res, next) => {
const { userId } = req.params;
if (!userId) {
next();
return;
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", (req, res) => {
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 || "/");
});
}

Expand All @@ -115,47 +141,58 @@ function configureAuthForServer(server) {
* @returns {Promise<undefined>} 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}]`);
}
}
}

Expand Down

0 comments on commit 8b3210f

Please sign in to comment.