Skip to content

Commit 83e28f9

Browse files
committed
feat: use standard OpenID logout flow
Signed-off-by: Eric Dobbertin <[email protected]>
1 parent 84452ce commit 83e28f9

File tree

4 files changed

+95
-72
lines changed

4 files changed

+95
-72
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +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
1112
OAUTH2_IDP_PUBLIC_CHANGE_PASSWORD_URL=http://localhost:4100/account/change-password?email=EMAIL&from=FROM
1213
OAUTH2_IDP_HOST_URL=http://identity.auth.reaction.localhost:4100
13-
OAUTH2_REDIRECT_URL=http://localhost:4000/callback
1414
OAUTH2_TOKEN_URL=http://hydra.auth.reaction.localhost:4444/oauth2/token
1515
PORT=4000
1616
SEGMENT_ANALYTICS_SKIP_MINIMIZE=true

src/components/AccountDropdown/AccountDropdown.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class AccountDropdown extends Component {
8383
Change Password
8484
</Button>
8585
</div>
86-
<Button color="primary" fullWidth href={`/logout/${account._id}`} variant="contained">
86+
<Button color="primary" fullWidth href="/logout" variant="contained">
8787
Sign Out
8888
</Button>
8989
</Fragment>

src/config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ if (process.env.IS_BUILDING_NEXTJS) {
3939
OAUTH2_CLIENT_SECRET: str(),
4040
OAUTH2_IDP_PUBLIC_CHANGE_PASSWORD_URL: url(),
4141
OAUTH2_IDP_HOST_URL: url(),
42-
OAUTH2_REDIRECT_URL: url(),
42+
OAUTH2_PUBLIC_LOGOUT_URL: url(),
4343
OAUTH2_TOKEN_URL: url(),
4444
PORT: port({ default: 4000 }),
4545
SEGMENT_ANALYTICS_SKIP_MINIMIZE: bool({ default: false }),

src/serverAuth.js

Lines changed: 92 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,29 @@ const config = require("./config");
44
const { decodeOpaqueId } = require("./lib/utils/decoding");
55
const logger = require("./lib/logger");
66

7+
let baseUrl = config.CANONICAL_URL;
8+
if (!baseUrl.endsWith("/")) baseUrl = `${baseUrl}/`;
9+
10+
const oauthRedirectUrl = `${baseUrl}callback`;
11+
const oauthPostLogoutRedirectUrl = `${baseUrl}post-logout-callback`;
12+
13+
/* eslint-disable camelcase */
14+
const storefrontHydraClient = {
15+
client_id: config.OAUTH2_CLIENT_ID,
16+
client_secret: config.OAUTH2_CLIENT_SECRET,
17+
grant_types: [
18+
"authorization_code",
19+
"refresh_token"
20+
],
21+
post_logout_redirect_uris: [oauthPostLogoutRedirectUrl],
22+
redirect_uris: [oauthRedirectUrl],
23+
response_types: ["code", "id_token", "token"],
24+
scope: "offline openid",
25+
subject_type: "public",
26+
token_endpoint_auth_method: "client_secret_post"
27+
};
28+
/* eslint-enable camelcase */
29+
730
// This is needed to allow custom parameters (e.g. loginActions) to be included
831
// when requesting authorization. This is setup to allow only loginAction to pass through
932
OAuth2Strategy.prototype.authorizationParams = function (options = {}) {
@@ -18,12 +41,12 @@ passport.use(
1841
tokenURL: config.OAUTH2_TOKEN_URL,
1942
clientID: config.OAUTH2_CLIENT_ID,
2043
clientSecret: config.OAUTH2_CLIENT_SECRET,
21-
callbackURL: config.OAUTH2_REDIRECT_URL,
44+
callbackURL: oauthRedirectUrl,
2245
state: true,
23-
scope: ["offline"]
46+
scope: ["offline", "openid"]
2447
},
25-
(accessToken, refreshToken, profile, cb) => {
26-
cb(null, { accessToken });
48+
(accessToken, refreshToken, params, profile, cb) => {
49+
cb(null, { accessToken, idToken: params.id_token });
2750
}
2851
)
2952
);
@@ -89,36 +112,25 @@ function configureAuthForServer(server) {
89112
res.redirect(url);
90113
});
91114

92-
server.get("/logout/:userId", (req, res, next) => {
93-
const { userId } = req.params;
94-
if (!userId) {
95-
next();
96-
return;
115+
server.get("/logout", (req, res, next) => {
116+
req.session.redirectTo = req.get("Referer");
117+
118+
const { idToken } = req.user || {};
119+
120+
// Clear storefront session auth
121+
req.logout();
122+
123+
if (idToken) {
124+
// Request log out of OAuth2 session
125+
res.redirect(`${config.OAUTH2_PUBLIC_LOGOUT_URL}?post_logout_redirect_uri=${oauthPostLogoutRedirectUrl}&id_token_hint=${idToken}`);
126+
} else {
127+
res.redirect(req.session.redirectTo || config.CANONICAL_URL);
97128
}
98-
const { id } = decodeOpaqueId(req.params.userId);
99-
100-
let urlBase = config.OAUTH2_IDP_HOST_URL;
101-
if (!urlBase.endsWith("/")) urlBase = `${urlBase}/`;
102-
103-
// Ask IDP to log us out
104-
fetch(`${urlBase}logout-user?userId=${id}`)
105-
.then((logoutResponse) => {
106-
if (logoutResponse.status >= 400) {
107-
const message = `Error from OAUTH2_IDP_HOST_URL logout endpoint: ${logoutResponse.status}. Check the HOST server settings`;
108-
109-
logger.error(message);
110-
res.status(logoutResponse.status).send(message);
111-
return;
112-
}
113-
// If IDP confirmed logout, clear login info on this side
114-
req.logout();
115-
res.redirect(req.get("Referer") || "/");
116-
return; // appease eslint consistent-return
117-
})
118-
.catch((error) => {
119-
logger.error(`Error while logging out: ${error}`);
120-
res.status(500).send(`Error while logging out: ${error.message}`);
121-
});
129+
});
130+
131+
server.get("/post-logout-callback", (req, res) => {
132+
// After success, redirect to the page we came from originally
133+
res.redirect(req.session.redirectTo || "/");
122134
});
123135
}
124136

@@ -130,47 +142,58 @@ function configureAuthForServer(server) {
130142
* @returns {Promise<undefined>} Nothing
131143
*/
132144
async function createHydraClientIfNecessary() {
133-
/* eslint-disable camelcase */
134-
const bodyEncoded = JSON.stringify({
135-
client_id: config.OAUTH2_CLIENT_ID,
136-
client_secret: config.OAUTH2_CLIENT_SECRET,
137-
grant_types: [
138-
"authorization_code",
139-
"refresh_token"
140-
],
141-
jwks: {},
142-
redirect_uris: [config.OAUTH2_REDIRECT_URL],
143-
response_types: ["token", "code"],
144-
scope: "offline",
145-
subject_type: "public",
146-
token_endpoint_auth_method: "client_secret_post"
147-
});
148-
/* eslint-enable camelcase */
149-
150145
let adminUrl = config.OAUTH2_ADMIN_URL;
151146
if (!adminUrl.endsWith("/")) adminUrl = `${adminUrl}/`;
152147

153-
logger.info("Creating Hydra client...");
154-
155-
const response = await fetch(`${adminUrl}clients`, {
156-
method: "POST",
157-
headers: { "Content-Type": "application/json" },
158-
body: bodyEncoded
148+
const getClientResponse = await fetch(`${adminUrl}clients/${config.OAUTH2_CLIENT_ID}`, {
149+
method: "GET",
150+
headers: { "Content-Type": "application/json" }
159151
});
160152

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

0 commit comments

Comments
 (0)