Skip to content

Commit 8b3210f

Browse files
authored
Merge pull request #637 from reactioncommerce/feat-aldeed-change-password
Change password and logout
2 parents d440385 + 138282f commit 8b3210f

File tree

4 files changed

+117
-73
lines changed

4 files changed

+117
-73
lines changed

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ OAUTH2_ADMIN_URL=http://hydra.auth.reaction.localhost:4445
77
OAUTH2_AUTH_URL=http://localhost:4444/oauth2/auth
88
OAUTH2_CLIENT_ID=example-storefront
99
OAUTH2_CLIENT_SECRET=CHANGEME
10+
OAUTH2_PUBLIC_LOGOUT_URL=http://localhost:4444/oauth2/sessions/logout
1011
OAUTH2_HOST=hydra.auth.reaction.localhost
12+
OAUTH2_IDP_PUBLIC_CHANGE_PASSWORD_URL=http://localhost:4100/account/change-password?email=EMAIL&from=FROM
1113
OAUTH2_IDP_HOST_URL=http://identity.auth.reaction.localhost:4100
12-
OAUTH2_REDIRECT_URL=http://localhost:4000/callback
1314
OAUTH2_TOKEN_URL=http://hydra.auth.reaction.localhost:4444/oauth2/token
1415
PORT=4000
1516
SEGMENT_ANALYTICS_SKIP_MINIMIZE=true

src/components/AccountDropdown/AccountDropdown.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@ class AccountDropdown extends Component {
7878
Profile
7979
</Button>
8080
</div>
81-
<Button color="primary" fullWidth href={`/logout/${account._id}`} variant="contained">
81+
<div className={classes.marginBottom}>
82+
<Button color="primary" fullWidth href={`/change-password?email=${encodeURIComponent(account.emailRecords[0].address)}`}>
83+
Change Password
84+
</Button>
85+
</div>
86+
<Button color="primary" fullWidth href="/logout" variant="contained">
8287
Sign Out
8388
</Button>
8489
</Fragment>

src/config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ if (process.env.IS_BUILDING_NEXTJS) {
3737
OAUTH2_AUTH_URL: url(),
3838
OAUTH2_CLIENT_ID: str(),
3939
OAUTH2_CLIENT_SECRET: str(),
40+
OAUTH2_IDP_PUBLIC_CHANGE_PASSWORD_URL: url(),
4041
OAUTH2_IDP_HOST_URL: url(),
41-
OAUTH2_REDIRECT_URL: url(),
42+
OAUTH2_PUBLIC_LOGOUT_URL: url(),
4243
OAUTH2_TOKEN_URL: url(),
4344
PORT: port({ default: 4000 }),
4445
SEGMENT_ANALYTICS_SKIP_MINIMIZE: bool({ default: false }),

src/serverAuth.js

Lines changed: 107 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,31 @@
11
const OAuth2Strategy = require("passport-oauth2");
22
const passport = require("passport");
33
const config = require("./config");
4-
const { decodeOpaqueId } = require("./lib/utils/decoding");
54
const logger = require("./lib/logger");
65

6+
let baseUrl = config.CANONICAL_URL;
7+
if (!baseUrl.endsWith("/")) baseUrl = `${baseUrl}/`;
8+
9+
const oauthRedirectUrl = `${baseUrl}callback`;
10+
const oauthPostLogoutRedirectUrl = `${baseUrl}post-logout-callback`;
11+
12+
/* eslint-disable camelcase */
13+
const storefrontHydraClient = {
14+
client_id: config.OAUTH2_CLIENT_ID,
15+
client_secret: config.OAUTH2_CLIENT_SECRET,
16+
grant_types: [
17+
"authorization_code",
18+
"refresh_token"
19+
],
20+
post_logout_redirect_uris: [oauthPostLogoutRedirectUrl],
21+
redirect_uris: [oauthRedirectUrl],
22+
response_types: ["code", "id_token", "token"],
23+
scope: "offline openid",
24+
subject_type: "public",
25+
token_endpoint_auth_method: "client_secret_post"
26+
};
27+
/* eslint-enable camelcase */
28+
729
// This is needed to allow custom parameters (e.g. loginActions) to be included
830
// when requesting authorization. This is setup to allow only loginAction to pass through
931
OAuth2Strategy.prototype.authorizationParams = function (options = {}) {
@@ -18,12 +40,12 @@ passport.use(
1840
tokenURL: config.OAUTH2_TOKEN_URL,
1941
clientID: config.OAUTH2_CLIENT_ID,
2042
clientSecret: config.OAUTH2_CLIENT_SECRET,
21-
callbackURL: config.OAUTH2_REDIRECT_URL,
43+
callbackURL: oauthRedirectUrl,
2244
state: true,
23-
scope: ["offline"]
45+
scope: ["offline", "openid"]
2446
},
25-
(accessToken, refreshToken, profile, cb) => {
26-
cb(null, { accessToken });
47+
(accessToken, refreshToken, params, profile, cb) => {
48+
cb(null, { accessToken, idToken: params.id_token });
2749
}
2850
)
2951
);
@@ -74,36 +96,40 @@ function configureAuthForServer(server) {
7496
res.redirect(req.session.redirectTo || "/");
7597
});
7698

77-
server.get("/logout/:userId", (req, res, next) => {
78-
const { userId } = req.params;
79-
if (!userId) {
80-
next();
81-
return;
99+
server.get("/change-password", (req, res) => {
100+
const { email } = req.query;
101+
102+
let from = req.get("Referer");
103+
if (typeof from !== "string" || from.length === 0) {
104+
from = config.CANONICAL_URL;
105+
}
106+
107+
let url = config.OAUTH2_IDP_PUBLIC_CHANGE_PASSWORD_URL;
108+
url = url.replace("EMAIL", encodeURIComponent(email || ""));
109+
url = url.replace("FROM", encodeURIComponent(from));
110+
111+
res.redirect(url);
112+
});
113+
114+
server.get("/logout", (req, res) => {
115+
req.session.redirectTo = req.get("Referer");
116+
117+
const { idToken } = req.user || {};
118+
119+
// Clear storefront session auth
120+
req.logout();
121+
122+
if (idToken) {
123+
// Request log out of OAuth2 session
124+
res.redirect(`${config.OAUTH2_PUBLIC_LOGOUT_URL}?post_logout_redirect_uri=${oauthPostLogoutRedirectUrl}&id_token_hint=${idToken}`);
125+
} else {
126+
res.redirect(req.session.redirectTo || config.CANONICAL_URL);
82127
}
83-
const { id } = decodeOpaqueId(req.params.userId);
84-
85-
let urlBase = config.OAUTH2_IDP_HOST_URL;
86-
if (!urlBase.endsWith("/")) urlBase = `${urlBase}/`;
87-
88-
// Ask IDP to log us out
89-
fetch(`${urlBase}logout-user?userId=${id}`)
90-
.then((logoutResponse) => {
91-
if (logoutResponse.status >= 400) {
92-
const message = `Error from OAUTH2_IDP_HOST_URL logout endpoint: ${logoutResponse.status}. Check the HOST server settings`;
93-
94-
logger.error(message);
95-
res.status(logoutResponse.status).send(message);
96-
return;
97-
}
98-
// If IDP confirmed logout, clear login info on this side
99-
req.logout();
100-
res.redirect(req.get("Referer") || "/");
101-
return; // appease eslint consistent-return
102-
})
103-
.catch((error) => {
104-
logger.error(`Error while logging out: ${error}`);
105-
res.status(500).send(`Error while logging out: ${error.message}`);
106-
});
128+
});
129+
130+
server.get("/post-logout-callback", (req, res) => {
131+
// After success, redirect to the page we came from originally
132+
res.redirect(req.session.redirectTo || "/");
107133
});
108134
}
109135

@@ -115,47 +141,58 @@ function configureAuthForServer(server) {
115141
* @returns {Promise<undefined>} Nothing
116142
*/
117143
async function createHydraClientIfNecessary() {
118-
/* eslint-disable camelcase */
119-
const bodyEncoded = JSON.stringify({
120-
client_id: config.OAUTH2_CLIENT_ID,
121-
client_secret: config.OAUTH2_CLIENT_SECRET,
122-
grant_types: [
123-
"authorization_code",
124-
"refresh_token"
125-
],
126-
jwks: {},
127-
redirect_uris: [config.OAUTH2_REDIRECT_URL],
128-
response_types: ["token", "code"],
129-
scope: "offline",
130-
subject_type: "public",
131-
token_endpoint_auth_method: "client_secret_post"
132-
});
133-
/* eslint-enable camelcase */
134-
135144
let adminUrl = config.OAUTH2_ADMIN_URL;
136145
if (!adminUrl.endsWith("/")) adminUrl = `${adminUrl}/`;
137146

138-
logger.info("Creating Hydra client...");
139-
140-
const response = await fetch(`${adminUrl}clients`, {
141-
method: "POST",
142-
headers: { "Content-Type": "application/json" },
143-
body: bodyEncoded
147+
const getClientResponse = await fetch(`${adminUrl}clients/${config.OAUTH2_CLIENT_ID}`, {
148+
method: "GET",
149+
headers: { "Content-Type": "application/json" }
144150
});
145151

146-
switch (response.status) {
147-
case 200:
148-
// intentional fallthrough!
149-
// eslint-disable-line no-fallthrough
150-
case 201:
151-
logger.info("OK: Hydra client created");
152-
break;
153-
case 409:
154-
logger.info("OK: Hydra client already exists");
155-
break;
156-
default:
157-
logger.error(await response.text());
158-
throw new Error(`Could not create Hydra client [${response.status}]`);
152+
if (![200, 404].includes(getClientResponse.status)) {
153+
logger.error(await getClientResponse.text());
154+
throw new Error(`Could not get Hydra client [${getClientResponse.status}]`);
155+
}
156+
157+
if (getClientResponse.status === 200) {
158+
// Update the client to be sure it has the latest config
159+
logger.info("Updating Hydra client...");
160+
161+
const updateClientResponse = await fetch(`${adminUrl}clients/${config.OAUTH2_CLIENT_ID}`, {
162+
method: "PUT",
163+
headers: { "Content-Type": "application/json" },
164+
body: JSON.stringify(storefrontHydraClient)
165+
});
166+
167+
if (updateClientResponse.status === 200) {
168+
logger.info("OK: Hydra client updated");
169+
} else {
170+
logger.error(await updateClientResponse.text());
171+
throw new Error(`Could not update Hydra client [${updateClientResponse.status}]`);
172+
}
173+
} else {
174+
logger.info("Creating Hydra client...");
175+
176+
const response = await fetch(`${adminUrl}clients`, {
177+
method: "POST",
178+
headers: { "Content-Type": "application/json" },
179+
body: JSON.stringify(storefrontHydraClient)
180+
});
181+
182+
switch (response.status) {
183+
case 200:
184+
// intentional fallthrough!
185+
// eslint-disable-line no-fallthrough
186+
case 201:
187+
logger.info("OK: Hydra client created");
188+
break;
189+
case 409:
190+
logger.info("OK: Hydra client already exists");
191+
break;
192+
default:
193+
logger.error(await response.text());
194+
throw new Error(`Could not create Hydra client [${response.status}]`);
195+
}
159196
}
160197
}
161198

0 commit comments

Comments
 (0)