Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move JWT from cookie store to Authorization header #2158

Merged
merged 31 commits into from
May 8, 2020

Conversation

radavis
Copy link
Contributor

@radavis radavis commented Apr 6, 2020

Resolves #2100

This pull request...

  • removes JWT from cookies
  • stores JWT on front end in localStorage
  • sends JWT in Authorization header with each request

This pull request is ready to merge when...

  • Tests have been updated (and all tests are passing)
  • This code has been reviewed by someone other than the original author
  • The experience passes a basic manual accessibility audit (keyboard nav, screenreader, text scaling) OR an exemption is documented no UI changes
  • The change has been documented
    • Associated OpenAPI documentation has been updated
  • Changelog is updated as appropriate

This feature is done when...

  • Design has approved the experience
  • Product has approved the experience

expiresIn: `${process.env.SESSION_LIFETIME_MINUTES}m`
}
),
signWebToken({ payload: session.content }),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted common functionality in this file into functions (signWebToken, verifyWebToken) within api/auth/jwtUtils.js

docker-compose.yml Outdated Show resolved Hide resolved
@cms-eapd-bot
Copy link

cms-eapd-bot commented Apr 7, 2020

This deploy was cleaned up.

@radavis radavis force-pushed the 2100-operation-cookie-crumble branch from cf9fc59 to f74b416 Compare May 1, 2020 20:42
@radavis
Copy link
Contributor Author

radavis commented May 1, 2020

Sorry this is such a massive change! 😬

@radavis radavis changed the title [WIP] Move JWT from cookie store to Authorization header Move JWT from cookie store to Authorization header May 4, 2020
Copy link
Contributor

@mgwalker mgwalker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only a few minor changes. I prefixed those comments with:

change

to make them easier to find. There's also one that's ripe for discussion about whether /auth/logout should ever return error statuses. On the whole, though, this PR looks really good. It's a nice, clean implementation!


const defaultStrategies = [new LocalStrategy(authenticate())];
const passportLocalStrategy = new LocalStrategy(authenticate());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a sensible change. The reason it was originally an array is we thought we would soon need to support more than one strategy - local for dev, and CMS's central auth service in production. That will eventually still be the case, but I like this being simpler in the meantime. 👍

api/auth/index.js Outdated Show resolved Hide resolved
'/auth/login',
passport.authenticate('local', { session: false }),
(req, res) => {
serializeUser(req.user, (err, sessionId) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since serializeUser is only used by our custom code now instead of being used by the Passport session library, it doesn't have to conform to that API anymore. We should look into how we can refactor it to be easier to use/maintain. (Added issue #2206 to track that as future work.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for documenting that!

// login routes.
const jwtExcludedRoutes = ['/auth/login/nonce', '/auth/login'];
app.use((req, res, next) =>
jwtExcludedRoutes.includes(req.originalUrl)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I wonder how this is affected by proxying. It's probably fine, but we should double-check in a preview deployment because those create a proxy from /api to /. If originalUrl is /api/auth/login/nonce, that could break this. But again... I bet it's fine. I think Express generally ignores proxies.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I was thinking about this yesterday evening. If I rely on the jwtMiddleware to only deserialize the user if the Authorization header is present (e.g. remove the res.send(400).end()), and rely on can and loggedIn to manage sending 401 and 403 responses, I can get rid of this funky excluded routes thing.

jwtExtractor,
signWebToken,
verifyWebToken
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting all of the JWT-related stuff in one place makes me so happy. 👍 👍

Comment on lines 147 to 148
400: {
description: 'Not logged in'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing with the error on logging out

Comment on lines +4 to +7
description: 'Not logged in'
},
403: {
description: 'Not logged in'
description: 'Does not have permission to this activity'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😂 Thank you!

Comment on lines +560 to +563
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'Bearer xxx.yyy.zzz'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍

docker-compose.yml Show resolved Hide resolved
const store = mockStore({});
fetchMock
.onPost('/auth/login/nonce', { username: 'name' })
.reply(200, { nonce: '123abc' });
fetchMock
.onPost('/auth/login', { username: '123abc', password: 'secret' })
.reply(200, { moop: 'moop', activities: [] });
.reply(200, { user: { moop: 'moop', activities: [] }, token: 'xxx.yyy.zzz' });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good ol' moop moop

@radavis radavis requested a review from mgwalker May 7, 2020 14:22
@radavis
Copy link
Contributor Author

radavis commented May 7, 2020

@mgwalker I'm pretty sure I've addressed all your requested changes. Please, take a look...

I have a few questions for you:

  • What do you think about the change to the jwtMiddleware?
  • Do you think there is any issue with the way I am handling logging out? Should logging out of one session end all of that user's sessions?
  • At what point in this process should I push my changes to the Wiki?

Copy link
Contributor

@mgwalker mgwalker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

return token;
const token = req.get('Authorization');
if (!token || !token.toLowerCase().match(/^bearer\s.+\..+\..+/)) return null;
const [temp, result] = token.split(' '); // eslint-disable-line no-unused-vars
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a future note, one convention for throwaway destructured values would be something like:

const [_, result] = ...

And then the linter won't complain. Only trouble is you can't do that if you have more than one throwaway variable. 😛

Comment on lines +80 to +87
const scenarios = [
['Bearer xxx.yyy.zzz', 'xxx.yyy.zzz', 'returns the JWT'],
['bearer xxx.yyy.zzz', 'xxx.yyy.zzz', 'returns the JWT'],
['bearer', null, 'returns null'],
['bearer ', null, 'returns null'],
['', null, 'returns null'],
['Elephanter xxx.yyy.zzz', null, 'returns null']
];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@radavis
Copy link
Contributor Author

radavis commented May 8, 2020

Notes to Design and Product: This is a back-end change, so you shouldn't see any changes in UI. Verify that this is functional by signing in and out. That's it!

@radavis radavis force-pushed the 2100-operation-cookie-crumble branch from 725a735 to 66952d4 Compare May 8, 2020 16:46
Copy link

@beparticular beparticular left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logged in and logged out. :)

@codecov-io
Copy link

codecov-io commented May 8, 2020

Codecov Report

Merging #2158 into master will not change coverage.
The diff coverage is n/a.

Impacted file tree graph

@@           Coverage Diff           @@
##           master    #2158   +/-   ##
=======================================
  Coverage   92.99%   92.99%           
=======================================
  Files         219      219           
  Lines        4214     4214           
  Branches      658      658           
=======================================
  Hits         3919     3919           
  Misses        273      273           
  Partials       22       22           

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 66952d4...66952d4. Read the comment docs.

Copy link
Contributor

@jeromeleecms jeromeleecms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can only verify this in Firefox -- but it looks like the rest of the team is good. Gtg.

@radavis radavis merged commit 0e1a350 into master May 8, 2020
@radavis radavis deleted the 2100-operation-cookie-crumble branch May 8, 2020 19:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Switch authentication from cookies to another mechanism
6 participants